diff --git a/docs/FileFormatSpec-1.5.fodt b/docs/FileFormatSpec-1.5.fodt
index a57c8627..4e2d66ef 100644
--- a/docs/FileFormatSpec-1.5.fodt
+++ b/docs/FileFormatSpec-1.5.fodt
@@ -1,31 +1,30 @@
- Veronica Olsen2022-11-05T18:25:44.8874731592023-06-03T18:35:47.268848685Veronica OlsenPT4H19S53LibreOffice/7.4.5.1$Linux_X86_64 LibreOffice_project/40$Build-1
+ Veronica Olsen2022-11-05T18:25:44.8874731592024-01-26T22:59:54.491819741Veronica OlsenPT4H2M7S55LibreOffice/7.4.7.2$Linux_X86_64 LibreOffice_project/40$Build-2
- 0
+ 35971
0
- 35645
+ 26083
20394
true
false
view2
- 15145
- 7216
+ 4692
+ 47456
0
- 0
- 35643
- 20392
+ 35971
+ 26081
+ 56363
0
1
false
140
false
false
- false
false
@@ -91,7 +90,7 @@
true
true
- 805029
+ 917862
true
false
@@ -117,7 +116,7 @@
false
false
- true
+ false
false
false
false
@@ -159,7 +158,7 @@
-
+
@@ -180,7 +179,7 @@
-
+
@@ -480,15 +479,6 @@
-
-
-
-
-
-
-
-
-
@@ -1108,153 +1098,148 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1271,78 +1256,81 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+
+
@@ -1420,17 +1408,20 @@
- novelWriter
- Project File Format 1.5 Specification
+ novelWriter
+ Project File Format 1.5 Specification
- This document covers the file format specification for the 1.5 file format for novelWriter project files. See the documentation1
- See: https://docs.novelwriter.io for a full description of the app’s functionality. The file format was intrioduced in Release 2.0.
- The target audience of this document is developers who intend to write a tool or script that generates valid novelWriter project files from data from other applications, or for templating purposes.
- Note that flags are generally written out as yes/no values, but also true/false and on/off are understood by the application. The None value that occasionally occurs in actual project files is the text representation of the Python None value, and indicates that there is no value set.
- Revisions
+ This document covers the file format specification for the 1.5 file format for novelWriter project files. See the documentation1
+ See: https://docs.novelwriter.io for a full description of the app’s functionality. The file format was intrioduced in Release 2.0.
+ The target audience of this document is developers who intend to write a tool or script that generates valid novelWriter project files from data from other applications, or for templating purposes.
+ Note that flags are generally written out as yes/no values, but also true/false and on/off are understood by the application. The None value that occasionally occurs in actual project files is the text representation of the Python None value, and indicates that there is no value set.
+ Revisions
- From Release 2.1 Beta 1: Removed the titleFormat node from settings.
+ From Release 2.1 Beta 1: Removed the titleFormat node from settings.
+
+
+ From Release 2.3 Beta 1: Removed the title node from project.
XML Root
@@ -1454,7 +1445,7 @@
- appVersion
+ appVersion
No
@@ -1465,7 +1456,7 @@
- hexVersion
+ hexVersion
No
@@ -1476,29 +1467,29 @@
- fileVersion
+ fileVersion
Yes
- The file format version used when writing the project file. This determines how the file is parsed, so it is important that it is set correctly. Valid values are “1.0”, “1.1”, “1.2”, “1.3”, “1.4”, and “1.5”.
+ The file format version used when writing the project file. This determines how the file is parsed, so it is important that it is set correctly. Valid values are “1.0”, “1.1”, “1.2”, “1.3”, “1.4”, and “1.5”.
- fileRevision
+ fileRevision
- No
+ No
- The revision of the file format used for non-breaking changes. This is an interger value.
+ The revision of the file format used for non-breaking changes. This is an interger value.
- timeStamp
+ timeStamp
No
@@ -1509,12 +1500,12 @@
Example
- <novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1"
- fileVersion="1.5" fileRevision="1"
- timeStamp="2022-11-05 17:46:51">
+ <novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1"
+ fileVersion="1.5" fileRevision="1"
+ timeStamp="2022-11-05 17:46:51">
Project Node
Tag: project
- The project section is a level one node under the root. It stores the primary settings for the project. The only required values are the id attribute and the name value. All other entries are set to default values during parsing if ommitted from the XML.
+ The project section is a level one node under the root. It stores the primary settings for the project. The only required values are the id attribute and the name value. All other entries are set to default values during parsing if ommitted from the XML.
Attributes
@@ -1533,18 +1524,18 @@
- id
+ id
- Yes
+ Yes
- A UUID that is unique for the project and does not change during the lifetime of the project. It is used by novelWriter to keep track of temporary files and to associate cross-project settings with specific projects without relying on the project name.
+ A UUID that is unique for the project and does not change during the lifetime of the project. It is used by novelWriter to keep track of temporary files and to associate cross-project settings with specific projects without relying on the project name.
- saveCount
+ saveCount
No
@@ -1555,7 +1546,7 @@
- autoCount
+ autoCount
No
@@ -1566,7 +1557,7 @@
- editTime
+ editTime
No
@@ -1594,49 +1585,37 @@
- name
+ name
- Yes
+ Yes
- The name the user has given to the project.
+ The name the user has given to the project.
- title
+ author
No
- The title the user has given to the project, which may differ from the project name. The value is used in some places in the user interface, and can be added to manuscript builds.
-
-
-
-
- author
-
-
- No
-
-
- The name of the novel author or authors. The value is used in some places in the user interface, and can be added to manuscript builds.
+ The name of the novel author or authors. The value is used in some places in the user interface, and can be added to manuscript builds.
Example
- <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea"
- saveCount="5" autoCount="10" editTime="1000">
- <name>Sample Project</name>
- <title>Sample Project</title>
- <author>Jane Smith</author>
+ <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea"
+ saveCount="5" autoCount="10" editTime="1000">
+ <name>Sample Project</name>
+ <author>Jane Smith</author>
</project>
-
- Settings Node
+
+ Settings Node
Tag: settings
- The settings section is a level one node under the root. It stores the various runtime values for the project, and the settings available from the Project Settings panel. None of these settings are required, and if missing, will be set to default values. The entire settings section can thus be ommitted.
+ The settings section is a level one node under the root. It stores the various runtime values for the project, and the settings available from the Project Settings panel. None of these settings are required, and if missing, will be set to default values. The entire settings section can thus be ommitted.
Attributes
None
Values
@@ -1657,18 +1636,18 @@
- doBackup
+ doBackup
- No
+ No
- User-controlled setting for whether or not to run backup when the project is closed.
+ User-controlled setting for whether or not to run backup when the project is closed.
- language
+ language
No
@@ -1679,7 +1658,7 @@
- spellChecking
+ spellChecking
No
@@ -1692,18 +1671,18 @@
- lastHandle
+ lastHandle
No
- An auto-generated list of the last documents open in the editor and viewer, and which novel root folder was last viewed in the Novel Tree and Outline View. These are saved as key/value pairs. See separate section for how they are stored as XML.
+ An auto-generated list of the last documents open in the editor and viewer, and which novel root folder was last viewed in the Novel Tree and Outline View. These are saved as key/value pairs. See separate section for how they are stored as XML.
- autoReplace
+ autoReplace
No
@@ -1714,50 +1693,50 @@
- titleFormat
+ titleFormat
No
- Removed in Revision 1.
+ Removed in Revision 1.
- status
+ status
No
- The status labels as defined by the user in Project Settings. These are saved as special key/value pars. See separate section for how they are stored as XML.
+ The status labels as defined by the user in Project Settings. These are saved as special key/value pars. See separate section for how they are stored as XML.
- importance
+ importance
No
- The importance labels as defined by the user in Project Settings. These are saved as special key/value pars. See separate section for how they are stored as XML.
+ The importance labels as defined by the user in Project Settings. These are saved as special key/value pars. See separate section for how they are stored as XML.
Example
- <settings>
- <doBackup>yes</doBackup>
- <language>en_GB</language>
- <spellChecking auto="yes">en_GB</spellChecking>
- <lastHandle />
- <autoReplace />
- <status />
- <importance />
- </settings>
-
- Key/Value Nodes
+ <settings>
+ <doBackup>yes</doBackup>
+ <language>en_GB</language>
+ <spellChecking auto="yes">en_GB</spellChecking>
+ <lastHandle />
+ <autoReplace />
+ <status />
+ <importance />
+ </settings>
+
+ Key/Value Nodes
The key/value nodes are used for lastHandle, autoReplace and titleFormat settings. The lookup key is stored as an attribute, and the value is the text of the node.
Attributes
@@ -1777,25 +1756,25 @@
- key
+ key
- Yes
+ Yes
- The lookup key.
+ The lookup key.
Example
- <lastHandle>
- <entry key="editor">636b6aa9b697b</entry>
- <entry key="viewer">636b6aa9b697b</entry>
- <entry key="novelTree">7031beac91f75</entry>
- <entry key="outline">7031beac91f75</entry>
- </lastHandle>
+ <lastHandle>
+ <entry key="editor">636b6aa9b697b</entry>
+ <entry key="viewer">636b6aa9b697b</entry>
+ <entry key="novelTree">7031beac91f75</entry>
+ <entry key="outline">7031beac91f75</entry>
+ </lastHandle>
Status/Importance Key/Value Nodes
- The status and importance settings are stored as key/value nodes with additional data attributes. The label of the status or importance setting is saved as the node text value.
+ The status and importance settings are stored as key/value nodes with additional data attributes. The label of the status or importance setting is saved as the node text value.
Attributes
@@ -1814,18 +1793,18 @@
- key
+ key
- Yes
+ Yes
- The lookup key. For status labels, it must consist of an “s” followed by six hexadecimal numbers. For importance labels it must consist of an “i” followed by six hexadecimal numbers. Each key string must be unique. Internally, they are random numbers, but also sequential values are valid.
+ The lookup key. For status labels, it must consist of an “s” followed by six hexadecimal numbers. For importance labels it must consist of an “i” followed by six hexadecimal numbers. Each key string must be unique. Internally, they are random numbers, but also sequential values are valid.
- count
+ count
No
@@ -1836,7 +1815,7 @@
- red
+ red
Yes
@@ -1847,48 +1826,48 @@
- green
+ green
Yes
- A number between 0 and 255 representing the green component of the label colour.
+ A number between 0 and 255 representing the green component of the label colour.
- blue
+ blue
Yes
- A number between 0 and 255 representing the blue component of the label colour.
+ A number between 0 and 255 representing the blue component of the label colour.
Example
- <status>
- <entry key="sf12341" count="4"
- red="100" green="100" blue="100">New</entry>
- <entry key="sd51c5b" count="0"
- red="193" green="129" blue="0">Draft</entry>
- <entry key="s78ea90" count="1"
- red="58" green="180" blue="58">Finished</entry>
- </status>
- <importance>
- <entry key="ia857f0" count="5"
- red="100" green="100" blue="100">None</entry>
- <entry key="icfb3a5" count="2"
- red="0" green="122" blue="188">Minor</entry>
- <entry key="i2d7a54" count="2"
- red="21" green="0" blue="180">Major</entry>
- </importance>
-
+ <status>
+ <entry key="sf12341" count="4"
+ red="100" green="100" blue="100">New</entry>
+ <entry key="sd51c5b" count="0"
+ red="193" green="129" blue="0">Draft</entry>
+ <entry key="s78ea90" count="1"
+ red="58" green="180" blue="58">Finished</entry>
+ </status>
+ <importance>
+ <entry key="ia857f0" count="5"
+ red="100" green="100" blue="100">None</entry>
+ <entry key="icfb3a5" count="2"
+ red="0" green="122" blue="188">Minor</entry>
+ <entry key="i2d7a54" count="2"
+ red="21" green="0" blue="180">Major</entry>
+ </importance>
+
Content Node
Tag: content
- The content section is a level one node under the root. It stores all the project items of the project. The items are stored in the order in which they appear in the project tree, so altering the order will affect the project structure. The attribute with the order number is not used when processing the data. It is written to the XML for debugging purposes.
+ The content section is a level one node under the root. It stores all the project items of the project. The items are stored in the order in which they appear in the project tree, so altering the order will affect the project structure. The attribute with the order number is not used when processing the data. It is written to the XML for debugging purposes.
Attributes
@@ -1907,35 +1886,35 @@
- items
+ items
No
- The number of project items in the content section.
+ The number of project items in the content section.
- novelWords
+ novelWords
No
- The number of words in total for all document nodes with layout DOCUMENT. The value defaults to 0, but this number forms the basis of computing writing statistics during a session, so the number should be properly set to ensure correct statistics.
+ The number of words in total for all document nodes with layout DOCUMENT. The value defaults to 0, but this number forms the basis of computing writing statistics during a session, so the number should be properly set to ensure correct statistics.
- notesWords
+ notesWords
No
- The number of words in total for all document nodes with layout NOTE. The value defaults to 0, but this number forms the basis of computing writing statistics during a session, so the number should be properly set to ensure correct statistics.
+ The number of words in total for all document nodes with layout NOTE. The value defaults to 0, but this number forms the basis of computing writing statistics during a session, so the number should be properly set to ensure correct statistics.
@@ -1957,31 +1936,31 @@
- item
+ item
- No
+ No
- A node representing a project item.
+ A node representing a project item.
Example
- <content items="27" novelWords="954" notesWords="409">
- <item handle="7031beac91f75" parent="None" root="7031beac91f75"
- order="0" type="ROOT" class="NOVEL">
- <meta expanded="yes"/>
- <name status="sc24b8f" import="ia857f0">Novel</name>
- </item>
- ...
- </content>
- Item Nodes
- The item nodes make up the actual project content of the project. Each node represents either a root folder, a regular folder, or a document. Each node has a type, class and layout setting that determine its category. Each item is given a handle that is a random hexadecimal string of length 13.
- For the items that are document files, the handle corresponds to its expected filename. It is therefore important that they match in generated projects. Each document node is expected to correspond to a file in the contents folder of the project named “content/7031beac91f75.nwd” for the item handle “7031beac91f75”. If there is no such file, the document item is assumed to contain no text, and a file is created when the user tries to save text to it.
- Each item node has a meta data node and a name node. The meta data node contains only collected information, and is thus not strictly required. However, an accurate word count and setting the correct icon in the project tree depend on these values. These values are set by the indexer class, so rebuilding the index should restore the data.
- The name node contains the primary user defined settings for an item, and is required for the entry to be valid.
-
+ <content items="27" novelWords="954" notesWords="409">
+ <item handle="7031beac91f75" parent="None" root="7031beac91f75"
+ order="0" type="ROOT" class="NOVEL">
+ <meta expanded="yes"/>
+ <name status="sc24b8f" import="ia857f0">Novel</name>
+ </item>
+ ...
+ </content>
+ Item Nodes
+ The item nodes make up the actual project content of the project. Each node represents either a root folder, a regular folder, or a document. Each node has a type, class and layout setting that determine its category. Each item is given a handle that is a random hexadecimal string of length 13.
+ For the items that are document files, the handle corresponds to its expected filename. It is therefore important that they match in generated projects. Each document node is expected to correspond to a file in the contents folder of the project named “content/7031beac91f75.nwd” for the item handle “7031beac91f75”. If there is no such file, the document item is assumed to contain no text, and a file is created when the user tries to save text to it.
+ Each item node has a meta data node and a name node. The meta data node contains only collected information, and is thus not strictly required. However, an accurate word count and setting the correct icon in the project tree depend on these values. These values are set by the indexer class, so rebuilding the index should restore the data.
+ The name node contains the primary user defined settings for an item, and is required for the entry to be valid.
+
Attributes
@@ -2000,79 +1979,79 @@
- handle
+ handle
- Yes
+ Yes
- The 13 value hexadecimal string that represents the item in the project. This is the primary identifier of a project item, and is required. It must be unique withing a project. Internally, it is generated as a random value, but it can also be sequential.
+ The 13 value hexadecimal string that represents the item in the project. This is the primary identifier of a project item, and is required. It must be unique withing a project. Internally, it is generated as a random value, but it can also be sequential.
- parent
+ parent
- Yes
+ Yes
- The handle of the parent item in the tree. This value should only be None for root folders. All other items must have a parent handle set. If the parent handle is None for an item that isn’t a root folder, the item will be treated as orphaned during project loading.
+ The handle of the parent item in the tree. This value should only be None for root folders. All other items must have a parent handle set. If the parent handle is None for an item that isn’t a root folder, the item will be treated as orphaned during project loading.
- root
+ root
- No
+ No
- The handle of the top of its hierarchy of parent items. That is, the root folder which it ultimately sits under. If the attribute is not set, it will be computed during loading. It is saved to the XML file for efficiency reasons.
+ The handle of the top of its hierarchy of parent items. That is, the root folder which it ultimately sits under. If the attribute is not set, it will be computed during loading. It is saved to the XML file for efficiency reasons.
- order
+ order
- No
+ No
- The numerical order of the item under its parent item. This value is not used during the loading process as the physical item order in the content node is used instead. It is primarily saved to the XML for debugging purposes.
+ The numerical order of the item under its parent item. This value is not used during the loading process as the physical item order in the content node is used instead. It is primarily saved to the XML for debugging purposes.
- type
+ type
- Yes
+ Yes
- The item type of the item node. Allowed values are ROOT, FOLDER and FILE.
+ The item type of the item node. Allowed values are ROOT, FOLDER and FILE.
- class
+ class
- Yes
+ Yes
- The item class of the item node. There are a number of item classes available in the app, all corresponding to a specific type of root folder. Each item in a root folder should have the same class set as the root folder itself. The attribute is not strictly required for items that aren’t root folders as it will be automatically set to match during loading. Allowed values are NOVEL, PLOT, CHARACTER, WORLD, TIMELINE, OBJECT, ENTITY, CUSTOM, ARCHIVE and TRASH.
+ The item class of the item node. There are a number of item classes available in the app, all corresponding to a specific type of root folder. Each item in a root folder should have the same class set as the root folder itself. The attribute is not strictly required for items that aren’t root folders as it will be automatically set to match during loading. Allowed values are NOVEL, PLOT, CHARACTER, WORLD, TIMELINE, OBJECT, ENTITY, CUSTOM, ARCHIVE and TRASH.
- layout
+ layout
- Yes
+ Yes
- The item layout of the item node. Allowed values are DOCUMENT and NOTE. This is the attribute that determines if a FILE type item is a Novel Document or a Project Note.
+ The item layout of the item node. Allowed values are DOCUMENT and NOTE. This is the attribute that determines if a FILE type item is a Novel Document or a Project Note.
@@ -2094,29 +2073,29 @@
- meta
+ meta
- No
+ No
- A node of meta data attributes for the item.
+ A node of meta data attributes for the item.
- name
+ name
- Yes
+ Yes
- A node of user settings for the item.
+ A node of user settings for the item.
- Meta Nodes
- The meta data collected for the current item. This data either represents the item’s last state in the app, or consists of data collected by the indexer. It can be restored by rebuilding the index, so the data is not essential.
+ Meta Nodes
+ The meta data collected for the current item. This data either represents the item’s last state in the app, or consists of data collected by the indexer. It can be restored by rebuilding the index, so the data is not essential.
Attributes
@@ -2135,73 +2114,73 @@
- expanded
+ expanded
- No
+ No
- Whether the tree node in the project tree was expanded or collapsed during the last session. Applies to all item types, but is ignored for items without child items.
+ Whether the tree node in the project tree was expanded or collapsed during the last session. Applies to all item types, but is ignored for items without child items.
- heading
+ heading
- No
+ No
- The heading level of the first heading of the text of the item. Only applies to FILE item types. Allowed values are H0, H1, H2, H3, and H4. Other values are reset to H0.
+ The heading level of the first heading of the text of the item. Only applies to FILE item types. Allowed values are H0, H1, H2, H3, and H4. Other values are reset to H0.
- charCount
+ charCount
- No
+ No
- The number of characters in the text of the item. Only applies to FILE item types.
+ The number of characters in the text of the item. Only applies to FILE item types.
- wordCount
+ wordCount
- No
+ No
- The number of words in the text of the item. Only applies to FILE item types.
+ The number of words in the text of the item. Only applies to FILE item types.
- paraCount
+ paraCount
- No
+ No
- The number of paragraphs in the text of the item. Only applies to FILE item types.
+ The number of paragraphs in the text of the item. Only applies to FILE item types.
- cursorPos
+ cursorPos
- No
+ No
- The last cursor position in the text of the item from the last editing session. Only applies to FILE item types. The value is used to restore the cursor position when the document is opened in the editor.
+ The last cursor position in the text of the item from the last editing session. Only applies to FILE item types. The value is used to restore the cursor position when the document is opened in the editor.
- Name Nodes
- The name node contains information about an item that is set by the user. Including its label, which is the text value of the node. This is the label that is displayed in the project tree.
+ Name Nodes
+ The name node contains information about an item that is set by the user. Including its label, which is the text value of the node. This is the label that is displayed in the project tree.
Attributes
@@ -2220,45 +2199,45 @@
- status
+ status
- No
+ No
- The ID of the status label that has been set for this item. Defaults to the first status item defined in the status section of the settings node.
+ The ID of the status label that has been set for this item. Defaults to the first status item defined in the status section of the settings node.
- import
+ import
- No
+ No
- The ID of the importance label that has been set for this item. Defaults to the first importance item defined in the importance section of the settings node.
+ The ID of the importance label that has been set for this item. Defaults to the first importance item defined in the importance section of the settings node.
- active
+ active
- No
+ No
- The active/inactive status of the document. Only applies to FILE item types.
+ The active/inactive status of the document. Only applies to FILE item types.
Example
- <item handle="53b69b83cdafc" parent="7031beac91f75" root="7031beac91f75"
- order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
- <meta expanded="no" heading="H1" charCount="93" wordCount="19"
- paraCount="2" cursorPos="119"/>
- <name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
- </item>
+ <item handle="53b69b83cdafc" parent="7031beac91f75" root="7031beac91f75"
+ order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
+ <meta expanded="no" heading="H1" charCount="93" wordCount="19"
+ paraCount="2" cursorPos="119"/>
+ <name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
+ </item>
\ No newline at end of file
diff --git a/docs/source/fileformatspec15.pdf b/docs/source/fileformatspec15.pdf
index 5115b5b1..58b5be9d 100644
Binary files a/docs/source/fileformatspec15.pdf and b/docs/source/fileformatspec15.pdf differ
diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py
index 4ce39cc6..a91f0b7f 100644
--- a/novelwriter/__init__.py
+++ b/novelwriter/__init__.py
@@ -42,9 +42,9 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
-__version__ = "2.3-alpha1"
-__hexversion__ = "0x020300a1"
-__date__ = "2023-12-17"
+__version__ = "2.3a2"
+__hexversion__ = "0x020300a2"
+__date__ = "2024-01-26"
__status__ = "Stable"
__domain__ = "novelwriter.io"
@@ -131,7 +131,7 @@ def main(sysArgs: list | None = None):
elif inOpt == "--debug":
CONFIG.isDebug = True
logLevel = logging.DEBUG
- logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}"
+ logFormat = "[{asctime:}] {filename:>18}:{lineno:<4d} {levelname:8} {message:}"
elif inOpt == "--style":
qtStyle = inArg
elif inOpt == "--config":
diff --git a/novelwriter/common.py b/novelwriter/common.py
index 988b6c78..59b6bbb5 100644
--- a/novelwriter/common.py
+++ b/novelwriter/common.py
@@ -250,6 +250,11 @@ def formatTime(t: int) -> str:
return "ERROR"
+def formatVersion(value: str) -> str:
+ """Format a version number into a more human readable form."""
+ return value.lower().replace("a", " Alpha ").replace("b", " Beta ").replace("rc", " RC ")
+
+
# =============================================================================================== #
# String Functions
# =============================================================================================== #
diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py
index 029bf22d..c88369e8 100644
--- a/novelwriter/core/coretools.py
+++ b/novelwriter/core/coretools.py
@@ -374,7 +374,6 @@ class ProjectBuilder:
project.data.setUuid(None)
project.data.setName(projName)
- project.data.setTitle(projName)
project.data.setAuthor(projAuthor)
project.data.setLanguage(projLang)
project.setDefaultStatusImport()
@@ -383,7 +382,7 @@ class ProjectBuilder:
# Add Root Folders
hNovelRoot = project.newRoot(nwItemClass.NOVEL)
hTitlePage = project.newFile(lblTitlePage, hNovelRoot)
- novelTitle = project.data.title if project.data.title else project.data.name
+ novelTitle = project.data.name
titlePage = f"#! {novelTitle}\n\n"
if project.data.author:
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index f58145f9..7f1598b6 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -528,17 +528,20 @@ class NWIndex:
yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem
return
- def getNovelWordCount(self, activeOnly: bool = True) -> int:
- """Count the number of words in the novel project."""
- wCount = 0
- for _, _, hItem in self._itemIndex.iterNovelStructure(activeOnly=activeOnly):
- wCount += hItem.wordCount
- return wCount
+ def getNovelWordCount(self, rootHandle: str | None = None, activeOnly: bool = True) -> int:
+ """Count the number of words in one or all novel roots."""
+ return sum(hItem.wordCount for _, _, hItem in self._itemIndex.iterNovelStructure(
+ rHandle=rootHandle, activeOnly=activeOnly
+ ))
- def getNovelTitleCounts(self, activeOnly: bool = True) -> list[int]:
- """Count the number of titles in the novel project."""
+ def getNovelTitleCounts(
+ self, rootHandle: str | None = None, activeOnly: bool = True
+ ) -> list[int]:
+ """Count the number of titles in one or all novel roots."""
hCount = [0, 0, 0, 0, 0]
- for _, _, hItem in self._itemIndex.iterNovelStructure(activeOnly=activeOnly):
+ for _, _, hItem in self._itemIndex.iterNovelStructure(
+ rHandle=rootHandle, activeOnly=activeOnly
+ ):
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
hCount[iLevel] += 1
return hCount
diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py
index 8e50c30e..c48aaa13 100644
--- a/novelwriter/core/options.py
+++ b/novelwriter/core/options.py
@@ -53,10 +53,6 @@ VALID_MAP: dict[str, set[str]] = {
"GuiProjectSettings": {
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW",
},
- "GuiProjectDetails": {
- "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
- "widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
- },
"GuiWordList": {"winWidth", "winHeight"},
"GuiNovelView": {"lastCol", "lastColSize"},
"GuiBuildSettings": {
@@ -71,7 +67,12 @@ VALID_MAP: dict[str, set[str]] = {
},
"GuiDocViewerPanel": {
"colWidths", "hideInactive",
- }
+ },
+ "GuiNovelDetails": {
+ "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
+ "widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
+ "novelRoot",
+ },
}
diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py
index 4a6fcec1..07c16909 100644
--- a/novelwriter/core/projectdata.py
+++ b/novelwriter/core/projectdata.py
@@ -53,7 +53,6 @@ class NWProjectData:
# Project Meta
self._uuid = ""
self._name = ""
- self._title = ""
self._author = ""
self._saveCount = 0
self._autoCount = 0
@@ -102,11 +101,6 @@ class NWProjectData:
"""Return the project name."""
return self._name
- @property
- def title(self) -> str:
- """Return the project title."""
- return self._title
-
@property
def author(self) -> str:
"""Return the project author."""
@@ -228,16 +222,9 @@ class NWProjectData:
self._project.setProjectChanged(True)
return
- def setTitle(self, value: str | None) -> None:
- """Set a new novel title."""
- if value != self._title:
- self._title = simplified(str(value or ""))
- self._project.setProjectChanged(True)
- return
-
def setAuthor(self, value: str | None) -> None:
"""Set the author value."""
- if value != self._title:
+ if value != self._author:
self._author = simplified(str(value or ""))
self._project.setProjectChanged(True)
return
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index 642a8f10..c64d290f 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -46,7 +46,7 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__)
FILE_VERSION = "1.5" # The current project file format version
-FILE_REVISION = "1" # The current project file format revision
+FILE_REVISION = "2" # The current project file format revision
HEX_VERSION = 0x0105
NUM_VERSION = {
@@ -105,7 +105,8 @@ class ProjectXMLReader:
the project or the content into their respective section nodes
as attributes. The id attribute was also added to the project.
- Rev 1: Drops the titleFormat section of settings.
+ Rev 1: Drops the titleFormat node from settings. 2.1 Beta 1.
+ Rev 2: Drops the title node from project. 2.3 Beta 1.
"""
def __init__(self, path: str | Path) -> None:
@@ -232,8 +233,6 @@ class ProjectXMLReader:
for xItem in xSection:
if xItem.tag == "name":
data.setName(xItem.text)
- elif xItem.tag == "title":
- data.setTitle(xItem.text)
elif xItem.tag == "author":
data.setAuthor(xItem.text)
else:
@@ -505,7 +504,6 @@ class ProjectXMLWriter:
xProject = ET.SubElement(xRoot, "project", attrib=projAttr)
self._packSingleValue(xProject, "name", data.name)
- self._packSingleValue(xProject, "title", data.title)
self._packSingleValue(xProject, "author", data.author)
# Save Project Settings
diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py
index c509a547..5598ece7 100644
--- a/novelwriter/core/tohtml.py
+++ b/novelwriter/core/tohtml.py
@@ -338,7 +338,6 @@ class ToHtml(Tokenizer):
data = {
"meta": {
"projectName": self._project.data.name,
- "novelTitle": self._project.data.title,
"novelAuthor": self._project.data.author,
"buildTime": int(timeStamp),
"buildTimeStr": formatTimeStamp(timeStamp),
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index f1421db1..5b222e73 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -759,7 +759,6 @@ class Tokenizer(ABC):
data = {
"meta": {
"projectName": self._project.data.name,
- "novelTitle": self._project.data.title,
"novelAuthor": self._project.data.author,
"buildTime": int(timeStamp),
"buildTimeStr": formatTimeStamp(timeStamp),
diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py
index b34a56bf..4409b595 100644
--- a/novelwriter/core/toodt.py
+++ b/novelwriter/core/toodt.py
@@ -283,9 +283,7 @@ class ToOdt(Tokenizer):
# ===============
if self._headerText == "":
- theTitle = self._project.data.title or self._project.data.name
- theAuth = self._project.data.author
- self._headerText = f"{theTitle} / {theAuth} /"
+ self._headerText = f"{self._project.data.name} / {self._project.data.author} /"
# Create Roots
# ============
@@ -373,7 +371,7 @@ class ToOdt(Tokenizer):
# Dublin Core Meta Data
xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "title"))
- xMeta.text = self._project.data.title or self._project.data.name
+ xMeta.text = self._project.data.name
xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "date"))
xMeta.text = timeStamp
diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py
index 69e19785..1329b06e 100644
--- a/novelwriter/dialogs/about.py
+++ b/novelwriter/dialogs/about.py
@@ -220,23 +220,25 @@ class GuiAbout(QDialog):
def _setStyleSheet(self) -> None:
"""Set stylesheet for all browser tabs."""
+ colHead = SHARED.theme.colHead
+ colKey = SHARED.theme.colKey
styleSheet = (
"h1, h2, h3, h4 {{"
- " color: rgb({hColR},{hColG},{hColB});"
+ " color: rgb({hColR}, {hColG}, {hColB});"
"}}\n"
"a {{"
- " color: rgb({hColR},{hColG},{hColB});"
+ " color: rgb({hColR}, {hColG}, {hColB});"
"}}\n"
".alt {{"
- " color: rgb({kColR},{kColG},{kColB});"
+ " color: rgb({kColR}, {kColG}, {kColB});"
"}}\n"
).format(
- hColR=SHARED.theme.colHead[0],
- hColG=SHARED.theme.colHead[1],
- hColB=SHARED.theme.colHead[2],
- kColR=SHARED.theme.colKey[0],
- kColG=SHARED.theme.colKey[1],
- kColB=SHARED.theme.colKey[2],
+ hColR=colHead.red(),
+ hColG=colHead.green(),
+ hColB=colHead.blue(),
+ kColR=colKey.red(),
+ kColG=colKey.green(),
+ kColB=colKey.blue(),
)
self.pageAbout.document().setDefaultStyleSheet(styleSheet)
self.pageNotes.document().setDefaultStyleSheet(styleSheet)
diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py
index 15b54660..a0c43611 100644
--- a/novelwriter/dialogs/docmerge.py
+++ b/novelwriter/dialogs/docmerge.py
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED
from novelwriter.extensions.switch import NSwitch
-from novelwriter.extensions.configlayout import NHelpLabel
+from novelwriter.extensions.configlayout import NColourLabel
logger = logging.getLogger(__name__)
@@ -54,9 +54,10 @@ class GuiDocMerge(QDialog):
self._data = {}
self.headLabel = QLabel("{0}".format(self.tr("Documents to Merge")))
- self.helpLabel = NHelpLabel(self.tr(
- "Drag and drop items to change the order, or uncheck to exclude."
- ), SHARED.theme.helpText)
+ self.helpLabel = NColourLabel(
+ self.tr("Drag and drop items to change the order, or uncheck to exclude."),
+ SHARED.theme.helpText, parent=self, wrap=True
+ )
iPx = SHARED.theme.baseIconSize
hSp = CONFIG.pxInt(12)
diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py
index 602b860c..b8add4c0 100644
--- a/novelwriter/dialogs/docsplit.py
+++ b/novelwriter/dialogs/docsplit.py
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED
from novelwriter.extensions.switch import NSwitch
-from novelwriter.extensions.configlayout import NHelpLabel
+from novelwriter.extensions.configlayout import NColourLabel
logger = logging.getLogger(__name__)
@@ -58,9 +58,9 @@ class GuiDocSplit(QDialog):
self.setWindowTitle(self.tr("Split Document"))
self.headLabel = QLabel("{0}".format(self.tr("Document Headers")))
- self.helpLabel = NHelpLabel(
+ self.helpLabel = NColourLabel(
self.tr("Select the maximum level to split into files."),
- SHARED.theme.helpText
+ SHARED.theme.helpText, parent=self, wrap=True
)
# Values
diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py
index 49e58e00..7653734e 100644
--- a/novelwriter/dialogs/preferences.py
+++ b/novelwriter/dialogs/preferences.py
@@ -26,11 +26,11 @@ from __future__ import annotations
import logging
-from PyQt5.QtGui import QCloseEvent, QColor, QFont, QKeyEvent, QKeySequence, QPalette
+from PyQt5.QtGui import QCloseEvent, QFont, QKeyEvent, QKeySequence
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QAbstractButton, QComboBox, QCompleter, QDialog, QDialogButtonBox,
- QDoubleSpinBox, QFileDialog, QFontDialog, QHBoxLayout, QLabel, QLineEdit,
+ QDoubleSpinBox, QFileDialog, QFontDialog, QHBoxLayout, QLineEdit,
QPushButton, QSpinBox, QToolButton, QVBoxLayout, QWidget, qApp
)
@@ -38,7 +38,7 @@ from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwConst, nwUnicode
from novelwriter.dialogs.quotes import GuiQuoteSelect
from novelwriter.extensions.switch import NSwitch
-from novelwriter.extensions.configlayout import NScrollableForm
+from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm
from novelwriter.extensions.pagedsidebar import NPagedSideBar
logger = logging.getLogger(__name__)
@@ -58,21 +58,10 @@ class GuiPreferences(QDialog):
self.resize(*CONFIG.preferencesWinSize)
# Title
- font = self.font()
- font.setPointSizeF(1.25*SHARED.theme.fontPointSize)
-
- palette = self.palette()
- palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.helpText))
-
- self.titleLabel = QLabel(self.tr("Preferences"), self)
- self.titleLabel.setFont(font)
- self.titleLabel.setPalette(palette)
- self.titleLabel.setIndent(CONFIG.pxInt(4))
-
- # SideBar
- self.sidebar = NPagedSideBar(self)
- self.sidebar.setLabelColor(SHARED.theme.helpText)
- self.sidebar.buttonClicked.connect(self._sidebarClicked)
+ self.titleLabel = NColourLabel(
+ self.tr("Preferences"), SHARED.theme.helpText,
+ parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
+ )
# Search Box
self.searchText = QLineEdit(self)
@@ -83,10 +72,10 @@ class GuiPreferences(QDialog):
)
self.searchAction.triggered.connect(self._gotoSearch)
- self.searchBox = QHBoxLayout()
- self.searchBox.addWidget(self.titleLabel)
- self.searchBox.addStretch(1)
- self.searchBox.addWidget(self.searchText, 1)
+ # SideBar
+ self.sidebar = NPagedSideBar(self)
+ self.sidebar.setLabelColor(SHARED.theme.helpText)
+ self.sidebar.buttonClicked.connect(self._sidebarClicked)
# Form
self.mainForm = NScrollableForm(self)
@@ -101,6 +90,11 @@ class GuiPreferences(QDialog):
self.buttonBox.clicked.connect(self._dialogButtonClicked)
# Assemble
+ self.searchBox = QHBoxLayout()
+ self.searchBox.addWidget(self.titleLabel)
+ self.searchBox.addStretch(1)
+ self.searchBox.addWidget(self.searchText, 1)
+
self.mainBox = QHBoxLayout()
self.mainBox.addWidget(self.sidebar)
self.mainBox.addWidget(self.mainForm)
diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py
deleted file mode 100644
index a6e12163..00000000
--- a/novelwriter/dialogs/projdetails.py
+++ /dev/null
@@ -1,518 +0,0 @@
-"""
-novelWriter – GUI Project Details
-=================================
-
-File History:
-Created: 2021-01-03 [1.1rc1] GuiProjectDetails
-
-This file is a part of novelWriter
-Copyright 2018–2024, Veronica Berglyd Olsen
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but
-WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-"""
-from __future__ import annotations
-
-import math
-import logging
-
-from PyQt5.QtGui import QCloseEvent, QFont
-from PyQt5.QtCore import Qt, QSize, pyqtSlot
-from PyQt5.QtWidgets import (
- QAbstractItemView, QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel,
- QLineEdit, QSpinBox, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
-)
-
-from novelwriter import CONFIG, SHARED
-from novelwriter.common import formatTime, numberToRoman
-from novelwriter.constants import nwUnicode
-from novelwriter.extensions.switch import NSwitch
-from novelwriter.extensions.pageddialog import NPagedDialog
-from novelwriter.extensions.novelselector import NovelSelector
-
-logger = logging.getLogger(__name__)
-
-
-class GuiProjectDetails(NPagedDialog):
-
- def __init__(self, parent: QWidget) -> None:
- super().__init__(parent=parent)
-
- logger.debug("Create: GuiProjectDetails")
- self.setObjectName("GuiProjectDetails")
-
- self.setWindowTitle(self.tr("Project Details"))
-
- wW = CONFIG.pxInt(600)
- wH = CONFIG.pxInt(400)
- pOptions = SHARED.project.options
-
- self.setMinimumWidth(wW)
- self.setMinimumHeight(wH)
- self.resize(
- CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "winWidth", wW)),
- CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "winHeight", wH))
- )
-
- self.tabMain = GuiProjectDetailsMain(self)
- self.tabContents = GuiProjectDetailsContents(self)
-
- self.addTab(self.tabMain, self.tr("Overview"))
- self.addTab(self.tabContents, self.tr("Contents"))
-
- self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
- self.buttonBox.rejected.connect(self.close)
- self.rejected.connect(self.close)
- self.addControls(self.buttonBox)
-
- logger.debug("Ready: GuiProjectDetails")
-
- return
-
- def __del__(self) -> None: # pragma: no cover
- logger.debug("Delete: GuiProjectDetails")
- return
-
- def updateValues(self) -> None:
- """Set all the values of the pages."""
- self.tabMain.updateValues()
- self.tabContents.updateValues()
- return
-
- ##
- # Events
- ##
-
- def closeEvent(self, event: QCloseEvent) -> None:
- """Capture the close event and perform cleanup."""
- self._saveGuiSettings()
- event.accept()
- self.deleteLater()
- return
-
- ##
- # Internal Functions
- ##
-
- def _saveGuiSettings(self) -> None:
- """Save GUI settings."""
- winWidth = CONFIG.rpxInt(self.width())
- winHeight = CONFIG.rpxInt(self.height())
-
- cColWidth = self.tabContents.getColumnSizes()
- widthCol0 = CONFIG.rpxInt(cColWidth[0])
- widthCol1 = CONFIG.rpxInt(cColWidth[1])
- widthCol2 = CONFIG.rpxInt(cColWidth[2])
- widthCol3 = CONFIG.rpxInt(cColWidth[3])
- widthCol4 = CONFIG.rpxInt(cColWidth[4])
-
- wordsPerPage = self.tabContents.wpValue.value()
- countFrom = self.tabContents.poValue.value()
- clearDouble = self.tabContents.dblValue.isChecked()
-
- logger.debug("Saving State: GuiProjectDetails")
- pOptions = SHARED.project.options
- pOptions.setValue("GuiProjectDetails", "winWidth", winWidth)
- pOptions.setValue("GuiProjectDetails", "winHeight", winHeight)
- pOptions.setValue("GuiProjectDetails", "widthCol0", widthCol0)
- pOptions.setValue("GuiProjectDetails", "widthCol1", widthCol1)
- pOptions.setValue("GuiProjectDetails", "widthCol2", widthCol2)
- pOptions.setValue("GuiProjectDetails", "widthCol3", widthCol3)
- pOptions.setValue("GuiProjectDetails", "widthCol4", widthCol4)
- pOptions.setValue("GuiProjectDetails", "wordsPerPage", wordsPerPage)
- pOptions.setValue("GuiProjectDetails", "countFrom", countFrom)
- pOptions.setValue("GuiProjectDetails", "clearDouble", clearDouble)
-
- return
-
-# END Class GuiProjectDetails
-
-
-class GuiProjectDetailsMain(QWidget):
-
- def __init__(self, parent: QWidget) -> None:
- super().__init__(parent=parent)
-
- fPx = SHARED.theme.fontPixelSize
- fPt = SHARED.theme.fontPointSize
- vPx = CONFIG.pxInt(4)
- hPx = CONFIG.pxInt(12)
-
- # Header
- # ======
-
- self.bookTitle = QLabel("")
- bookFont = self.bookTitle.font()
- bookFont.setPointSizeF(2.2*fPt)
- bookFont.setWeight(QFont.Bold)
- self.bookTitle.setFont(bookFont)
- self.bookTitle.setAlignment(Qt.AlignHCenter)
- self.bookTitle.setWordWrap(True)
-
- self.projName = QLabel("")
- workFont = self.projName.font()
- workFont.setPointSizeF(0.8*fPt)
- workFont.setItalic(True)
- self.projName.setFont(workFont)
- self.projName.setAlignment(Qt.AlignHCenter)
- self.projName.setWordWrap(True)
-
- self.bookAuthors = QLabel("")
- authFont = self.bookAuthors.font()
- authFont.setPointSizeF(1.2*fPt)
- self.bookAuthors.setFont(authFont)
- self.bookAuthors.setAlignment(Qt.AlignHCenter)
- self.bookAuthors.setWordWrap(True)
-
- # Stats
- # =====
-
- self.wordCountLbl = QLabel("%s:" % self.tr("Words"))
- self.wordCountVal = QLabel("")
-
- self.chapCountLbl = QLabel("%s:" % self.tr("Chapters"))
- self.chapCountVal = QLabel("")
-
- self.sceneCountLbl = QLabel("%s:" % self.tr("Scenes"))
- self.sceneCountVal = QLabel("")
-
- self.revCountLbl = QLabel("%s:" % self.tr("Revisions"))
- self.revCountVal = QLabel("")
-
- self.editTimeLbl = QLabel("%s:" % self.tr("Editing Time"))
- self.editTimeVal = QLabel("")
-
- self.statsGrid = QGridLayout()
- self.statsGrid.addWidget(self.wordCountLbl, 0, 0, 1, 1, Qt.AlignRight)
- self.statsGrid.addWidget(self.wordCountVal, 0, 1, 1, 1, Qt.AlignLeft)
- self.statsGrid.addWidget(self.chapCountLbl, 1, 0, 1, 1, Qt.AlignRight)
- self.statsGrid.addWidget(self.chapCountVal, 1, 1, 1, 1, Qt.AlignLeft)
- self.statsGrid.addWidget(self.sceneCountLbl, 2, 0, 1, 1, Qt.AlignRight)
- self.statsGrid.addWidget(self.sceneCountVal, 2, 1, 1, 1, Qt.AlignLeft)
- self.statsGrid.addWidget(self.revCountLbl, 3, 0, 1, 1, Qt.AlignRight)
- self.statsGrid.addWidget(self.revCountVal, 3, 1, 1, 1, Qt.AlignLeft)
- self.statsGrid.addWidget(self.editTimeLbl, 4, 0, 1, 1, Qt.AlignRight)
- self.statsGrid.addWidget(self.editTimeVal, 4, 1, 1, 1, Qt.AlignLeft)
- self.statsGrid.setHorizontalSpacing(hPx)
- self.statsGrid.setVerticalSpacing(vPx)
-
- # Meta
- # ====
-
- self.projPathLbl = QLabel("%s:" % self.tr("Path"))
- self.projPathVal = QLineEdit()
- self.projPathVal.setReadOnly(True)
-
- self.projPathBox = QHBoxLayout()
- self.projPathBox.addWidget(self.projPathLbl)
- self.projPathBox.addWidget(self.projPathVal)
- self.projPathBox.setSpacing(hPx)
-
- # Assemble
- # ========
-
- self.outerBox = QVBoxLayout()
- self.outerBox.addSpacing(fPx)
- self.outerBox.addWidget(self.bookTitle)
- self.outerBox.addWidget(self.projName)
- self.outerBox.addWidget(self.bookAuthors)
- self.outerBox.addSpacing(2*fPx)
- self.outerBox.addLayout(self.statsGrid)
- self.outerBox.addSpacing(fPx)
- self.outerBox.addStretch(1)
- self.outerBox.addLayout(self.projPathBox)
-
- self.setLayout(self.outerBox)
-
- return
-
- def updateValues(self) -> None:
- """Set all the values."""
- project = SHARED.project
- pIndex = project.index
- hCounts = pIndex.getNovelTitleCounts()
- nwCount = pIndex.getNovelWordCount()
- edTime = project.currentEditTime
-
- self.bookTitle.setText(project.data.title or project.data.name)
- self.projName.setText(self.tr("Project: {0}").format(project.data.name))
- self.bookAuthors.setText(self.tr("By {0}").format(project.data.author))
-
- self.wordCountVal.setText(f"{nwCount:n}")
- self.chapCountVal.setText(f"{hCounts[2]:n}")
- self.sceneCountVal.setText(f"{hCounts[3]:n}")
- self.revCountVal.setText(f"{project.data.saveCount:n}")
- self.editTimeVal.setText(formatTime(edTime))
-
- self.projPathVal.setText(str(project.storage.storagePath))
-
- return
-
-# END Class GuiProjectDetailsMain
-
-
-class GuiProjectDetailsContents(QWidget):
-
- C_TITLE = 0
- C_WORDS = 1
- C_PAGES = 2
- C_PAGE = 3
- C_PROG = 4
-
- def __init__(self, parent: QWidget) -> None:
- super().__init__(parent=parent)
-
- # Internal
- self._theToC = []
- self._currentRoot = None
-
- iPx = SHARED.theme.baseIconSize
- hPx = CONFIG.pxInt(12)
- vPx = CONFIG.pxInt(4)
- pOptions = SHARED.project.options
-
- # Header
- # ======
-
- self.tocLabel = QLabel("%s" % self.tr("Table of Contents"))
-
- self.novelValue = NovelSelector(self)
- self.novelValue.setMinimumWidth(CONFIG.pxInt(200))
- self.novelValue.novelSelectionChanged.connect(self._novelValueChanged)
-
- self.headBox = QHBoxLayout()
- self.headBox.addWidget(self.tocLabel)
- self.headBox.addWidget(self.novelValue)
-
- # Contents Tree
- # =============
-
- self.tocTree = QTreeWidget()
- self.tocTree.setIconSize(QSize(iPx, iPx))
- self.tocTree.setIndentation(0)
- self.tocTree.setColumnCount(6)
- self.tocTree.setSelectionMode(QAbstractItemView.NoSelection)
- self.tocTree.setHeaderLabels([
- self.tr("Title"),
- self.tr("Words"),
- self.tr("Pages"),
- self.tr("Page"),
- self.tr("Progress"),
- ""
- ])
-
- treeHeadItem = self.tocTree.headerItem()
- if treeHeadItem:
- treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
- treeHeadItem.setTextAlignment(self.C_PAGES, Qt.AlignRight)
- treeHeadItem.setTextAlignment(self.C_PAGE, Qt.AlignRight)
- treeHeadItem.setTextAlignment(self.C_PROG, Qt.AlignRight)
-
- treeHeader = self.tocTree.header()
- treeHeader.setStretchLastSection(True)
- treeHeader.setMinimumSectionSize(hPx)
-
- wCol0 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol0", 200))
- wCol1 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol1", 60))
- wCol2 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol2", 60))
- wCol3 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol3", 60))
- wCol4 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol4", 90))
-
- self.tocTree.setColumnWidth(0, wCol0)
- self.tocTree.setColumnWidth(1, wCol1)
- self.tocTree.setColumnWidth(2, wCol2)
- self.tocTree.setColumnWidth(3, wCol3)
- self.tocTree.setColumnWidth(4, wCol4)
- self.tocTree.setColumnWidth(5, hPx)
-
- # Options
- # =======
-
- wordsPerPage = pOptions.getInt("GuiProjectDetails", "wordsPerPage", 350)
- countFrom = pOptions.getInt("GuiProjectDetails", "countFrom", 1)
- clearDouble = pOptions.getBool("GuiProjectDetails", "clearDouble", True)
-
- wordsHelp = (
- self.tr("Typical word count for a 5 by 8 inch book page with 11 pt font is 350.")
- )
- offsetHelp = (
- self.tr("Start counting page numbers from this page.")
- )
- dblHelp = (
- self.tr("Assume a new chapter or partition always start on an odd numbered page.")
- )
-
- self.wpLabel = QLabel(self.tr("Words per page"))
- self.wpLabel.setToolTip(wordsHelp)
-
- self.wpValue = QSpinBox()
- self.wpValue.setMinimum(10)
- self.wpValue.setMaximum(1000)
- self.wpValue.setSingleStep(10)
- self.wpValue.setValue(wordsPerPage)
- self.wpValue.setToolTip(wordsHelp)
- self.wpValue.valueChanged.connect(self._populateTree)
-
- self.poLabel = QLabel(self.tr("Count pages from"))
- self.poLabel.setToolTip(offsetHelp)
-
- self.poValue = QSpinBox()
- self.poValue.setMinimum(1)
- self.poValue.setMaximum(9999)
- self.poValue.setSingleStep(1)
- self.poValue.setValue(countFrom)
- self.poValue.setToolTip(offsetHelp)
- self.poValue.valueChanged.connect(self._populateTree)
-
- self.dblLabel = QLabel(self.tr("Clear double pages"))
- self.dblLabel.setToolTip(dblHelp)
-
- self.dblValue = NSwitch(self, 2*iPx, iPx)
- self.dblValue.setChecked(clearDouble)
- self.dblValue.setToolTip(dblHelp)
- self.dblValue.clicked.connect(self._populateTree)
-
- self.optionsBox = QGridLayout()
- self.optionsBox.addWidget(self.wpLabel, 0, 0)
- self.optionsBox.addWidget(self.wpValue, 0, 1)
- self.optionsBox.addWidget(self.dblLabel, 0, 3)
- self.optionsBox.addWidget(self.dblValue, 0, 4)
- self.optionsBox.addWidget(self.poLabel, 1, 0)
- self.optionsBox.addWidget(self.poValue, 1, 1)
- self.optionsBox.setHorizontalSpacing(hPx)
- self.optionsBox.setVerticalSpacing(vPx)
- self.optionsBox.setColumnStretch(2, 1)
-
- # Assemble
- # ========
-
- self.outerBox = QVBoxLayout()
- self.outerBox.addLayout(self.headBox)
- self.outerBox.addWidget(self.tocTree)
- self.outerBox.addLayout(self.optionsBox)
-
- self.setLayout(self.outerBox)
-
- return
-
- def getColumnSizes(self) -> list[int]:
- """Return the column widths for the tree columns."""
- retVals = [
- self.tocTree.columnWidth(0),
- self.tocTree.columnWidth(1),
- self.tocTree.columnWidth(2),
- self.tocTree.columnWidth(3),
- self.tocTree.columnWidth(4),
- ]
- return retVals
-
- def updateValues(self) -> None:
- """Populate the tree."""
- self._currentRoot = None
- self.novelValue.updateList()
- self.novelValue.setHandle(self.novelValue.firstHandle)
- self._prepareData(self.novelValue.firstHandle)
- self._populateTree()
- return
-
- ##
- # Internal Functions
- ##
-
- def _prepareData(self, rootHandle: str | None) -> None:
- """Extract the information from the project index."""
- logger.debug("Populating ToC from handle '%s'", rootHandle)
- self._theToC = SHARED.project.index.getTableOfContents(rootHandle, 2)
- self._theToC.append(("", 0, self.tr("END"), 0))
- return
-
- ##
- # Slots
- ##
-
- @pyqtSlot(str)
- def _novelValueChanged(self, tHandle: str) -> None:
- """Refresh the tree with another root item."""
- if tHandle != self._currentRoot:
- self._prepareData(tHandle)
- self._populateTree()
- self._currentRoot = self.novelValue.handle
- return
-
- @pyqtSlot()
- def _populateTree(self) -> None:
- """Set the content of the chapter/page tree."""
- dblPages = self.dblValue.isChecked()
- wpPage = self.wpValue.value()
- fstPage = self.poValue.value() - 1
-
- pTotal = 0
- tPages = 1
-
- theList = []
- for _, tLevel, tTitle, wCount in self._theToC:
- pCount = math.ceil(wCount/wpPage)
- if dblPages:
- pCount += pCount%2
-
- pTotal += pCount
- theList.append((tLevel, tTitle, wCount, pCount))
-
- pMax = pTotal - fstPage
-
- self.tocTree.clear()
- for tLevel, tTitle, wCount, pCount in theList:
- newItem = QTreeWidgetItem()
-
- if tPages <= fstPage:
- progPage = numberToRoman(tPages, True)
- progText = ""
- else:
- cPage = tPages - fstPage
- pgProg = 100.0*(cPage - 1)/pMax if pMax > 0 else 0.0
- progPage = f"{cPage:n}"
- progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%"
-
- hDec = SHARED.theme.getHeaderDecoration(tLevel)
- if tTitle.strip() == "":
- tTitle = self.tr("Untitled")
-
- newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
- newItem.setText(self.C_TITLE, tTitle)
- newItem.setText(self.C_WORDS, f"{wCount:n}")
- newItem.setText(self.C_PAGES, f"{pCount:n}")
- newItem.setText(self.C_PAGE, progPage)
- newItem.setText(self.C_PROG, progText)
-
- newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
- newItem.setTextAlignment(self.C_PAGES, Qt.AlignRight)
- newItem.setTextAlignment(self.C_PAGE, Qt.AlignRight)
- newItem.setTextAlignment(self.C_PROG, Qt.AlignRight)
-
- # Make pages and titles/partitions stand out
- if tLevel < 2:
- bFont = newItem.font(self.C_TITLE)
- if tLevel == 0:
- bFont.setItalic(True)
- else:
- bFont.setBold(True)
- bFont.setUnderline(True)
- newItem.setFont(self.C_TITLE, bFont)
-
- tPages += pCount
-
- self.tocTree.addTopLevelItem(newItem)
-
- return
-
-# END Class GuiProjectDetailsContents
diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projectsettings.py
similarity index 57%
rename from novelwriter/dialogs/projsettings.py
rename to novelwriter/dialogs/projectsettings.py
index 8a9354a0..f6ff7730 100644
--- a/novelwriter/dialogs/projsettings.py
+++ b/novelwriter/dialogs/projectsettings.py
@@ -3,7 +3,8 @@ novelWriter – GUI Project Settings
==================================
File History:
-Created: 2018-09-29 [0.0.1] GuiProjectSettings
+Created: 2018-09-29 [0.0.1] GuiProjectSettings
+Rewritten: 2024-01-26 [2.3b1] GuiProjectSettings
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
@@ -25,75 +26,101 @@ from __future__ import annotations
import logging
-from typing import TYPE_CHECKING
-
-from PyQt5.QtGui import QCloseEvent, QIcon, QPixmap, QColor
+from PyQt5.QtGui import QCloseEvent, QColor, QIcon, QPixmap
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
- QColorDialog, QComboBox, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit,
- QPushButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, qApp
+ QColorDialog, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, QLineEdit,
+ QPushButton, QStackedWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
+ QWidget, qApp
)
from novelwriter import CONFIG, SHARED
from novelwriter.common import simplified
from novelwriter.extensions.switch import NSwitch
-from novelwriter.extensions.pageddialog import NPagedDialog
-from novelwriter.extensions.configlayout import NConfigLayout
-
-if TYPE_CHECKING: # pragma: no cover
- from novelwriter.guimain import GuiMain
+from novelwriter.extensions.configlayout import NColourLabel, NFixedPage, NScrollableForm
+from novelwriter.extensions.pagedsidebar import NPagedSideBar
logger = logging.getLogger(__name__)
-class GuiProjectSettings(NPagedDialog):
+class GuiProjectSettings(QDialog):
- TAB_MAIN = 0
- TAB_STATUS = 1
- TAB_IMPORT = 2
- TAB_REPLACE = 3
+ PAGE_SETTINGS = 0
+ PAGE_STATUS = 1
+ PAGE_IMPORT = 2
+ PAGE_REPLACE = 3
- newProjectSettingsReady = pyqtSignal()
+ newProjectSettingsReady = pyqtSignal(bool)
- def __init__(self, mainGui: GuiMain, focusTab: int = TAB_MAIN) -> None:
- super().__init__(parent=mainGui)
+ def __init__(self, parent: QWidget, gotoPage: int = PAGE_SETTINGS) -> None:
+ super().__init__(parent=parent)
logger.debug("Create: GuiProjectSettings")
self.setObjectName("GuiProjectSettings")
-
- self.mainGui = mainGui
- SHARED.project.countStatus()
self.setWindowTitle(self.tr("Project Settings"))
- wW = CONFIG.pxInt(570)
- wH = CONFIG.pxInt(375)
- pOptions = SHARED.project.options
-
- self.setMinimumWidth(wW)
- self.setMinimumHeight(wH)
+ options = SHARED.project.options
+ self.setMinimumSize(CONFIG.pxInt(500), CONFIG.pxInt(400))
self.resize(
- CONFIG.pxInt(pOptions.getInt("GuiProjectSettings", "winWidth", wW)),
- CONFIG.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH))
+ CONFIG.pxInt(options.getInt("GuiProjectSettings", "winWidth", CONFIG.pxInt(650))),
+ CONFIG.pxInt(options.getInt("GuiProjectSettings", "winHeight", CONFIG.pxInt(500)))
)
- self.tabMain = GuiProjectEditMain(self)
- self.tabStatus = GuiProjectEditStatus(self, True)
- self.tabImport = GuiProjectEditStatus(self, False)
- self.tabReplace = GuiProjectEditReplace(self)
+ # Title
+ self.titleLabel = NColourLabel(
+ self.tr("Project Settings"), SHARED.theme.helpText,
+ parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
+ )
- self.addTab(self.tabMain, self.tr("Settings"))
- self.addTab(self.tabStatus, self.tr("Status"))
- self.addTab(self.tabImport, self.tr("Importance"))
- self.addTab(self.tabReplace, self.tr("Auto-Replace"))
+ # SideBar
+ self.sidebar = NPagedSideBar(self)
+ self.sidebar.setLabelColor(SHARED.theme.helpText)
+ self.sidebar.addButton(self.tr("Settings"), self.PAGE_SETTINGS)
+ self.sidebar.addButton(self.tr("Status"), self.PAGE_STATUS)
+ self.sidebar.addButton(self.tr("Importance"), self.PAGE_IMPORT)
+ self.sidebar.addButton(self.tr("Auto-Replace"), self.PAGE_REPLACE)
+ self.sidebar.setSelected(gotoPage)
+ self.sidebar.buttonClicked.connect(self._sidebarClicked)
- self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ # Buttons
+ self.buttonBox = QDialogButtonBox(
+ QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel
+ )
self.buttonBox.accepted.connect(self._doSave)
self.buttonBox.rejected.connect(self.close)
- self.rejected.connect(self.close)
- self.addControls(self.buttonBox)
- # Focus Tab
- self._focusTab(focusTab)
+ # Content
+ SHARED.project.countStatus()
+
+ self.settingsPage = _SettingsPage(self)
+ self.statusPage = _StatusPage(self, True)
+ self.importPage = _StatusPage(self, False)
+ self.replacePage = _ReplacePage(self)
+
+ self.mainStack = QStackedWidget(self)
+ self.mainStack.addWidget(self.settingsPage)
+ self.mainStack.addWidget(self.statusPage)
+ self.mainStack.addWidget(self.importPage)
+ self.mainStack.addWidget(self.replacePage)
+
+ # Assemble
+ self.topBox = QHBoxLayout()
+ self.topBox.addWidget(self.titleLabel)
+ self.topBox.addStretch(1)
+
+ self.mainBox = QHBoxLayout()
+ self.mainBox.addWidget(self.sidebar)
+ self.mainBox.addWidget(self.mainStack)
+ self.mainBox.setContentsMargins(0, 0, 0, 0)
+
+ self.outerBox = QVBoxLayout()
+ self.outerBox.addLayout(self.topBox)
+ self.outerBox.addLayout(self.mainBox)
+ self.outerBox.addWidget(self.buttonBox)
+ self.outerBox.setSpacing(CONFIG.pxInt(8))
+
+ self.setLayout(self.outerBox)
+ self.setSizeGripEnabled(True)
logger.debug("Ready: GuiProjectSettings")
@@ -103,9 +130,13 @@ class GuiProjectSettings(NPagedDialog):
logger.debug("Delete: GuiProjectSettings")
return
+ ##
+ # Events
+ ##
+
def closeEvent(self, event: QCloseEvent) -> None:
- """Capture the close event and perform cleanup."""
- self._saveGuiSettings()
+ """Capture the user closing the window and save settings."""
+ self._saveSettings()
event.accept()
self.deleteLater()
return
@@ -114,40 +145,52 @@ class GuiProjectSettings(NPagedDialog):
# Private Slots
##
+ @pyqtSlot(int)
+ def _sidebarClicked(self, pageId: int) -> None:
+ """Process a user request to switch page."""
+ if pageId == self.PAGE_SETTINGS:
+ self.mainStack.setCurrentWidget(self.settingsPage)
+ elif pageId == self.PAGE_STATUS:
+ self.mainStack.setCurrentWidget(self.statusPage)
+ elif pageId == self.PAGE_IMPORT:
+ self.mainStack.setCurrentWidget(self.importPage)
+ elif pageId == self.PAGE_REPLACE:
+ self.mainStack.setCurrentWidget(self.replacePage)
+ return
+
@pyqtSlot()
def _doSave(self) -> None:
"""Save settings and close dialog."""
project = SHARED.project
- projName = self.tabMain.editName.text()
- bookTitle = self.tabMain.editTitle.text()
- bookAuthor = self.tabMain.editAuthor.text()
- projLang = self.tabMain.projLang.currentData()
- spellLang = self.tabMain.spellLang.currentData()
- doBackup = not self.tabMain.doBackup.isChecked()
+ projName = self.settingsPage.projName.text()
+ projAuthor = self.settingsPage.projAuthor.text()
+ projLang = self.settingsPage.projLang.currentData()
+ spellLang = self.settingsPage.spellLang.currentData()
+ doBackup = not self.settingsPage.doBackup.isChecked()
project.data.setName(projName)
- project.data.setTitle(bookTitle)
- project.data.setAuthor(bookAuthor)
+ project.data.setAuthor(projAuthor)
project.data.setDoBackup(doBackup)
project.data.setSpellLang(spellLang)
project.setProjectLang(projLang)
- if self.tabStatus.colChanged:
- newList, delList = self.tabStatus.getNewList()
+ rebuildTrees = False
+
+ if self.statusPage.wasChanged:
+ newList, delList = self.statusPage.getNewList()
project.setStatusColours(newList, delList)
+ rebuildTrees = True
- if self.tabImport.colChanged:
- newList, delList = self.tabImport.getNewList()
+ if self.importPage.wasChanged:
+ newList, delList = self.importPage.getNewList()
project.setImportColours(newList, delList)
+ rebuildTrees = True
- if self.tabStatus.colChanged or self.tabImport.colChanged:
- self.mainGui.rebuildTrees()
-
- if self.tabReplace.arChanged:
- newList = self.tabReplace.getNewList()
+ if self.replacePage.wasChanged:
+ newList = self.replacePage.getNewList()
project.data.setAutoReplace(newList)
- self.newProjectSettingsReady.emit()
+ self.newProjectSettingsReady.emit(rebuildTrees)
qApp.processEvents()
self.close()
@@ -157,134 +200,103 @@ class GuiProjectSettings(NPagedDialog):
# Internal Functions
##
- def _focusTab(self, tab: int) -> None:
- """Change which is the focused tab."""
- if tab == self.TAB_MAIN:
- self.setCurrentWidget(self.tabMain)
- elif tab == self.TAB_STATUS:
- self.setCurrentWidget(self.tabStatus)
- elif tab == self.TAB_IMPORT:
- self.setCurrentWidget(self.tabImport)
- elif tab == self.TAB_REPLACE:
- self.setCurrentWidget(self.tabReplace)
- return
-
- def _saveGuiSettings(self) -> None:
+ def _saveSettings(self) -> None:
"""Save GUI settings."""
winWidth = CONFIG.rpxInt(self.width())
winHeight = CONFIG.rpxInt(self.height())
- replaceColW = CONFIG.rpxInt(self.tabReplace.listBox.columnWidth(0))
- statusColW = CONFIG.rpxInt(self.tabStatus.listBox.columnWidth(0))
- importColW = CONFIG.rpxInt(self.tabImport.listBox.columnWidth(0))
+ statusColW = CONFIG.rpxInt(self.statusPage.columnWidth())
+ importColW = CONFIG.rpxInt(self.importPage.columnWidth())
+ replaceColW = CONFIG.rpxInt(self.replacePage.columnWidth())
logger.debug("Saving State: GuiProjectSettings")
- pOptions = SHARED.project.options
- pOptions.setValue("GuiProjectSettings", "winWidth", winWidth)
- pOptions.setValue("GuiProjectSettings", "winHeight", winHeight)
- pOptions.setValue("GuiProjectSettings", "replaceColW", replaceColW)
- pOptions.setValue("GuiProjectSettings", "statusColW", statusColW)
- pOptions.setValue("GuiProjectSettings", "importColW", importColW)
+ options = SHARED.project.options
+ options.setValue("GuiProjectSettings", "winWidth", winWidth)
+ options.setValue("GuiProjectSettings", "winHeight", winHeight)
+ options.setValue("GuiProjectSettings", "statusColW", statusColW)
+ options.setValue("GuiProjectSettings", "importColW", importColW)
+ options.setValue("GuiProjectSettings", "replaceColW", replaceColW)
return
# END Class GuiProjectSettings
-class GuiProjectEditMain(QWidget):
+class _SettingsPage(NScrollableForm):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
- # The Form
- self.mainForm = NConfigLayout()
- self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
- self.setLayout(self.mainForm)
+ xW = CONFIG.pxInt(200)
+ data = SHARED.project.data
+ self.setHelpTextStyle(SHARED.theme.helpText)
+ self.setRowIndent(0)
- self.mainForm.addGroupLabel(self.tr("Project Settings"))
-
- xW = CONFIG.pxInt(250)
- pData = SHARED.project.data
-
- self.editName = QLineEdit()
- self.editName.setMaxLength(200)
- self.editName.setMaximumWidth(xW)
- self.editName.setText(pData.name)
- self.mainForm.addRow(
- self.tr("Project name"),
- self.editName,
- self.tr("Should be set only once.")
+ # Project Name
+ self.projName = QLineEdit(self)
+ self.projName.setMaxLength(200)
+ self.projName.setMinimumWidth(xW)
+ self.projName.setText(data.name)
+ self.addRow(
+ self.tr("Project name"), self.projName,
+ self.tr("Changing this will affect the backup path."),
+ stretch=(3, 2)
)
- self.editTitle = QLineEdit()
- self.editTitle.setMaxLength(200)
- self.editTitle.setMaximumWidth(xW)
- self.editTitle.setText(pData.title)
- self.mainForm.addRow(
- self.tr("Novel title"),
- self.editTitle,
- self.tr("Change whenever you want!")
- )
-
- self.editAuthor = QLineEdit()
- self.editAuthor.setMaxLength(200)
- self.editAuthor.setMaximumWidth(xW)
- self.editAuthor.setText(pData.author)
- self.mainForm.addRow(
- self.tr("Author(s)"),
- self.editAuthor,
- self.tr("Change whenever you want!")
+ # Project Author
+ self.projAuthor = QLineEdit(self)
+ self.projAuthor.setMaxLength(200)
+ self.projAuthor.setMinimumWidth(xW)
+ self.projAuthor.setText(data.author)
+ self.addRow(
+ self.tr("Author(s)"), self.projAuthor,
+ self.tr("Only used when building the manuscript."),
+ stretch=(3, 2)
)
+ # Project Language
self.projLang = QComboBox(self)
- self.projLang.setMaximumWidth(xW)
+ self.projLang.setMinimumWidth(xW)
for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ):
self.projLang.addItem(language, tag)
- self.mainForm.addRow(
- self.tr("Project language"),
- self.projLang,
- self.tr("Used when building the manuscript.")
+ self.addRow(
+ self.tr("Project language"), self.projLang,
+ self.tr("Only used when building the manuscript."),
+ stretch=(3, 2)
)
+ if (idx := self.projLang.findData(data.language)) != -1:
+ self.projLang.setCurrentIndex(idx)
- langIdx = 0
- if pData.language is not None:
- langIdx = self.projLang.findData(pData.language)
- if langIdx == -1:
- langIdx = self.projLang.findData("en_GB")
- if langIdx != -1:
- self.projLang.setCurrentIndex(langIdx)
-
+ # Spell Check Language
self.spellLang = QComboBox(self)
- self.spellLang.setMaximumWidth(xW)
+ self.spellLang.setMinimumWidth(xW)
self.spellLang.addItem(self.tr("Default"), "None")
if CONFIG.hasEnchant:
for tag, language in SHARED.spelling.listDictionaries():
self.spellLang.addItem(language, tag)
- self.mainForm.addRow(
- self.tr("Spell check language"),
- self.spellLang,
- self.tr("Overrides main preferences.")
+ self.addRow(
+ self.tr("Spell check language"), self.spellLang,
+ self.tr("Overrides main preferences."),
+ stretch=(3, 2)
)
+ if (idx := self.spellLang.findData(data.spellLang)) != -1:
+ self.spellLang.setCurrentIndex(idx)
- langIdx = 0
- if pData.spellLang is not None:
- langIdx = self.spellLang.findData(pData.spellLang)
- if langIdx != -1:
- self.spellLang.setCurrentIndex(langIdx)
-
+ # Backup on Close
self.doBackup = NSwitch(self)
- self.doBackup.setChecked(not pData.doBackup)
- self.mainForm.addRow(
- self.tr("No backup on close"),
- self.doBackup,
+ self.doBackup.setChecked(not data.doBackup)
+ self.addRow(
+ self.tr("Disable backup on close"), self.doBackup,
self.tr("Overrides main preferences.")
)
+ self.finalise()
+
return
-# END Class GuiProjectEditMain
+# END Class _SettingsPage
-class GuiProjectEditStatus(QWidget):
+class _StatusPage(NFixedPage):
COL_LABEL = 0
COL_USAGE = 1
@@ -297,56 +309,54 @@ class GuiProjectEditStatus(QWidget):
super().__init__(parent=parent)
if isStatus:
- self.theStatus = SHARED.project.data.itemStatus
- pageLabel = self.tr("Novel File Status Levels")
+ status = SHARED.project.data.itemStatus
+ pageLabel = self.tr("Novel Document Status Levels")
colSetting = "statusColW"
else:
- self.theStatus = SHARED.project.data.itemImport
- pageLabel = self.tr("Note File Importance Levels")
+ status = SHARED.project.data.itemImport
+ pageLabel = self.tr("Project Note Importance Levels")
colSetting = "importColW"
wCol0 = CONFIG.pxInt(
SHARED.project.options.getInt("GuiProjectSettings", colSetting, 130)
)
- self.colDeleted = []
- self.colChanged = False
- self.selColour = QColor(100, 100, 100)
+ self._changed = False
+ self._colDeleted = []
+ self._selColour = QColor(100, 100, 100)
self.iPx = SHARED.theme.baseIconSize
- # The List
- # ========
+ # Title
+ self.pageTitle = NColourLabel(
+ pageLabel, SHARED.theme.helpText, parent=self,
+ scale=NColourLabel.HEADER_SCALE
+ )
- self.listBox = QTreeWidget()
- self.listBox.setHeaderLabels([
- self.tr("Label"), self.tr("Usage"),
- ])
+ # List Box
+ self.listBox = QTreeWidget(self)
+ self.listBox.setHeaderLabels([self.tr("Label"), self.tr("Usage")])
self.listBox.itemSelectionChanged.connect(self._selectedItem)
self.listBox.setColumnWidth(self.COL_LABEL, wCol0)
self.listBox.setIndentation(0)
- for key, entry in self.theStatus.items():
+ for key, entry in status.items():
self._addItem(key, entry["name"], entry["cols"], entry["count"])
# List Controls
- # =============
-
- self.addButton = QPushButton(SHARED.theme.getIcon("add"), "")
+ self.addButton = QPushButton(SHARED.theme.getIcon("add"), "", self)
self.addButton.clicked.connect(self._newItem)
- self.delButton = QPushButton(SHARED.theme.getIcon("remove"), "")
+ self.delButton = QPushButton(SHARED.theme.getIcon("remove"), "", self)
self.delButton.clicked.connect(self._delItem)
- self.upButton = QPushButton(SHARED.theme.getIcon("up"), "")
+ self.upButton = QPushButton(SHARED.theme.getIcon("up"), "", self)
self.upButton.clicked.connect(lambda: self._moveItem(-1))
- self.dnButton = QPushButton(SHARED.theme.getIcon("down"), "")
+ self.dnButton = QPushButton(SHARED.theme.getIcon("down"), "", self)
self.dnButton.clicked.connect(lambda: self._moveItem(1))
# Edit Form
- # =========
-
self.editName = QLineEdit()
self.editName.setMaxLength(40)
self.editName.setPlaceholderText(self.tr("Select item to edit"))
@@ -364,8 +374,6 @@ class GuiProjectEditStatus(QWidget):
self.saveButton.clicked.connect(self._saveItem)
# Assemble
- # ========
-
self.listControls = QVBoxLayout()
self.listControls.addWidget(self.addButton)
self.listControls.addWidget(self.delButton)
@@ -387,16 +395,25 @@ class GuiProjectEditStatus(QWidget):
self.innerBox.addLayout(self.listControls)
self.outerBox = QVBoxLayout()
- self.outerBox.addWidget(QLabel("%s" % pageLabel))
+ self.outerBox.addWidget(self.pageTitle)
self.outerBox.addLayout(self.innerBox)
- self.setLayout(self.outerBox)
+ self.setCentralLayout(self.outerBox)
return
+ @property
+ def wasChanged(self) -> bool:
+ """The user changed these settings."""
+ return self._changed
+
+ ##
+ # Methods
+ ##
+
def getNewList(self) -> tuple[list, list]:
"""Return list of entries."""
- if self.colChanged:
+ if self._changed:
newList = []
for n in range(self.listBox.topLevelItemCount()):
item = self.listBox.topLevelItem(n)
@@ -406,10 +423,13 @@ class GuiProjectEditStatus(QWidget):
"name": item.text(self.COL_LABEL),
"cols": item.data(self.COL_LABEL, self.COL_ROLE),
})
- return newList, self.colDeleted
-
+ return newList, self._colDeleted
return [], []
+ def columnWidth(self) -> int:
+ """Return the size of the header column."""
+ return self.listBox.columnWidth(0)
+
##
# Private Slots
##
@@ -417,12 +437,12 @@ class GuiProjectEditStatus(QWidget):
@pyqtSlot()
def _selectColour(self) -> None:
"""Open a dialog to select the status icon colour."""
- if self.selColour is not None:
+ if self._selColour is not None:
newCol = QColorDialog.getColor(
- self.selColour, self, self.tr("Select Colour")
+ self._selColour, self, self.tr("Select Colour")
)
if newCol.isValid():
- self.selColour = newCol
+ self._selColour = newCol
pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(newCol)
self.colButton.setIcon(QIcon(pixmap))
@@ -433,7 +453,7 @@ class GuiProjectEditStatus(QWidget):
def _newItem(self) -> None:
"""Create a new status item."""
self._addItem(None, self.tr("New Item"), (100, 100, 100), 0)
- self.colChanged = True
+ self._changed = True
return
@pyqtSlot()
@@ -446,8 +466,8 @@ class GuiProjectEditStatus(QWidget):
SHARED.error(self.tr("Cannot delete a status item that is in use."))
else:
self.listBox.takeTopLevelItem(iRow)
- self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE))
- self.colChanged = True
+ self._colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE))
+ self._changed = True
return
@pyqtSlot()
@@ -458,9 +478,9 @@ class GuiProjectEditStatus(QWidget):
selItem.setText(self.COL_LABEL, simplified(self.editName.text()))
selItem.setIcon(self.COL_LABEL, self.colButton.icon())
selItem.setData(self.COL_LABEL, self.COL_ROLE, (
- self.selColour.red(), self.selColour.green(), self.selColour.blue()
+ self._selColour.red(), self._selColour.green(), self._selColour.blue()
))
- self.colChanged = True
+ self._changed = True
return
@pyqtSlot()
@@ -474,7 +494,7 @@ class GuiProjectEditStatus(QWidget):
name = selItem.text(self.COL_LABEL)
pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(QColor(*cols))
- self.selColour = QColor(*cols)
+ self._selColour = QColor(*cols)
self.editName.setText(name)
self.colButton.setIcon(QIcon(pixmap))
self.editName.selectAll()
@@ -485,7 +505,7 @@ class GuiProjectEditStatus(QWidget):
else:
pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(QColor(100, 100, 100))
- self.selColour = QColor(100, 100, 100)
+ self._selColour = QColor(100, 100, 100)
self.editName.setText("")
self.colButton.setIcon(QIcon(pixmap))
self.editName.setEnabled(False)
@@ -498,16 +518,16 @@ class GuiProjectEditStatus(QWidget):
##
def _addItem(self, key: str | None, name: str,
- cols: tuple[int, int, int], count: int) -> None:
+ colour: tuple[int, int, int], count: int) -> None:
"""Add a status item to the list."""
pixmap = QPixmap(self.iPx, self.iPx)
- pixmap.fill(QColor(*cols))
+ pixmap.fill(QColor(*colour))
item = QTreeWidgetItem()
item.setText(self.COL_LABEL, name)
item.setIcon(self.COL_LABEL, QIcon(pixmap))
item.setData(self.COL_LABEL, self.KEY_ROLE, key)
- item.setData(self.COL_LABEL, self.COL_ROLE, cols)
+ item.setData(self.COL_LABEL, self.COL_ROLE, colour)
item.setData(self.COL_LABEL, self.NUM_ROLE, count)
item.setText(self.COL_USAGE, self._usageString(count))
@@ -533,30 +553,29 @@ class GuiProjectEditStatus(QWidget):
if cItem is not None:
cItem.setSelected(True)
- self.colChanged = True
+ self._changed = True
return
def _getSelectedItem(self) -> QTreeWidgetItem | None:
"""Get the currently selected item."""
- selItem = self.listBox.selectedItems()
- if len(selItem) > 0:
- return selItem[0]
+ if items := self.listBox.selectedItems():
+ return items[0]
return None
- def _usageString(self, nUse: int) -> str:
+ def _usageString(self, count: int) -> str:
"""Generate usage string."""
- if nUse == 0:
+ if count == 0:
return self.tr("Not in use")
- elif nUse == 1:
+ elif count == 1:
return self.tr("Used once")
else:
- return self.tr("Used by {0} items").format(nUse)
+ return self.tr("Used by {0} items").format(count)
-# END Class GuiProjectEditStatus
+# END Class _StatusPage
-class GuiProjectEditReplace(QWidget):
+class _ReplacePage(NFixedPage):
COL_KEY = 0
COL_REPL = 1
@@ -564,24 +583,24 @@ class GuiProjectEditReplace(QWidget):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
- self.arChanged = False
+ self._changed = False
wCol0 = CONFIG.pxInt(
SHARED.project.options.getInt("GuiProjectSettings", "replaceColW", 130)
)
- pageLabel = self.tr("Text Replace List for Preview and Export")
+
+ # Title
+ self.pageTitle = NColourLabel(
+ self.tr("Text Auto-Replace for Preview and Build"),
+ SHARED.theme.helpText, parent=self, scale=NColourLabel.HEADER_SCALE
+ )
# List Box
- # ========
-
self.listBox = QTreeWidget()
- self.listBox.setHeaderLabels([
- self.tr("Keyword"),
- self.tr("Replace With"),
- ])
- self.listBox.itemSelectionChanged.connect(self._selectedItem)
+ self.listBox.setHeaderLabels([self.tr("Keyword"), self.tr("Replace With")])
self.listBox.setColumnWidth(self.COL_KEY, wCol0)
self.listBox.setIndentation(0)
+ self.listBox.itemSelectionChanged.connect(self._selectedItem)
for aKey, aVal in SHARED.project.data.autoReplace.items():
newItem = QTreeWidgetItem(["<%s>" % aKey, aVal])
@@ -591,8 +610,6 @@ class GuiProjectEditReplace(QWidget):
self.listBox.setSortingEnabled(True)
# List Controls
- # =============
-
self.addButton = QPushButton(SHARED.theme.getIcon("add"), "")
self.addButton.clicked.connect(self._addEntry)
@@ -600,8 +617,6 @@ class GuiProjectEditReplace(QWidget):
self.delButton.clicked.connect(self._delEntry)
# Edit Form
- # =========
-
self.editKey = QLineEdit()
self.editKey.setPlaceholderText(self.tr("Select item to edit"))
self.editKey.setEnabled(False)
@@ -615,8 +630,6 @@ class GuiProjectEditReplace(QWidget):
self.saveButton.clicked.connect(self._saveEntry)
# Assemble
- # ========
-
self.listControls = QVBoxLayout()
self.listControls.addWidget(self.addButton)
self.listControls.addWidget(self.delButton)
@@ -636,50 +649,61 @@ class GuiProjectEditReplace(QWidget):
self.innerBox.addLayout(self.listControls)
self.outerBox = QVBoxLayout()
- self.outerBox.addWidget(QLabel("%s" % pageLabel))
+ self.outerBox.addWidget(self.pageTitle)
self.outerBox.addLayout(self.innerBox)
- self.setLayout(self.outerBox)
+ self.setCentralLayout(self.outerBox)
return
+ @property
+ def wasChanged(self) -> bool:
+ """The user changed these settings."""
+ return self._changed
+
+ ##
+ # Methods
+ ##
+
def getNewList(self) -> dict:
"""Extract the list from the widget."""
new = {}
for n in range(self.listBox.topLevelItemCount()):
- tItem = self.listBox.topLevelItem(n)
- if tItem is not None:
+ if tItem := self.listBox.topLevelItem(n):
aKey = self._stripNotAllowed(tItem.text(0))
aVal = tItem.text(1)
if len(aKey) > 0:
new[aKey] = aVal
return new
+ def columnWidth(self) -> int:
+ """Return the size of the header column."""
+ return self.listBox.columnWidth(0)
+
##
- # Internal Functions
+ # Private Slots
##
- def _selectedItem(self) -> bool:
+ @pyqtSlot()
+ def _selectedItem(self) -> None:
"""Extract the details from the selected item and populate the
edit form.
"""
- selItem = self._getSelectedItem()
- if selItem is None:
- return False
- editKey = self._stripNotAllowed(selItem.text(0))
- editVal = selItem.text(1)
- self.editKey.setText(editKey)
- self.editValue.setText(editVal)
- self.editKey.setEnabled(True)
- self.editValue.setEnabled(True)
- self.editKey.selectAll()
- self.editKey.setFocus()
- return True
+ if selItem := self._getSelectedItem():
+ editKey = self._stripNotAllowed(selItem.text(0))
+ editVal = selItem.text(1)
+ self.editKey.setText(editKey)
+ self.editValue.setText(editVal)
+ self.editKey.setEnabled(True)
+ self.editValue.setEnabled(True)
+ self.editKey.selectAll()
+ self.editKey.setFocus()
+ return
+ @pyqtSlot()
def _saveEntry(self) -> None:
"""Save the form data into the list widget."""
- selItem = self._getSelectedItem()
- if selItem:
+ if selItem := self._getSelectedItem():
newKey = self.editKey.text()
newVal = self.editValue.text()
saveKey = self._stripNotAllowed(newKey)
@@ -691,38 +715,36 @@ class GuiProjectEditReplace(QWidget):
self.editKey.setEnabled(False)
self.editValue.setEnabled(False)
self.listBox.clearSelection()
- self.arChanged = True
+ self._changed = True
return
+ @pyqtSlot()
def _addEntry(self) -> None:
"""Add a new list entry."""
saveKey = "" % (self.listBox.topLevelItemCount() + 1)
- newVal = ""
- newItem = QTreeWidgetItem([saveKey, newVal])
- self.listBox.addTopLevelItem(newItem)
+ self.listBox.addTopLevelItem(QTreeWidgetItem([saveKey, ""]))
return
+ @pyqtSlot()
def _delEntry(self) -> None:
"""Delete the selected entry."""
- selItem = self._getSelectedItem()
- if selItem:
+ if selItem := self._getSelectedItem():
self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(selItem))
- self.arChanged = True
+ self._changed = True
return
+ ##
+ # Internal Functions
+ ##
+
def _getSelectedItem(self) -> QTreeWidgetItem | None:
"""Extract the currently selected item."""
- selItem = self.listBox.selectedItems()
- if len(selItem) == 0:
- return None
- return selItem[0]
+ if items := self.listBox.selectedItems():
+ return items[0]
+ return None
def _stripNotAllowed(self, key: str) -> str:
"""Clean up the replace key string."""
- result = ""
- for c in key:
- if c.isalnum():
- result += c
- return result
+ return "".join(c for c in key if c.isalnum())
-# END Class GuiProjectEditReplace
+# END Class _ReplacePage
diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py
index d714cf00..99d38c03 100644
--- a/novelwriter/extensions/configlayout.py
+++ b/novelwriter/extensions/configlayout.py
@@ -3,9 +3,11 @@ novelWriter – Custom Widget: Config Layout
==========================================
File History:
-Created: 2020-05-03 [0.4.5] NConfigLayout, NHelpLabel
+Created: 2020-05-03 [0.4.5] NConfigLayout, NColourLabel
Created: 2023-05-23 [2.1b1] NSimpleLayout
Created: 2024-01-08 [2.3b1] NScrollableForm
+Created: 2024-01-26 [2.3b1] NScrollablePage
+Created: 2024-01-26 [2.3b1] NFixedPage
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
@@ -28,27 +30,74 @@ from __future__ import annotations
from PyQt5.QtGui import QColor, QPalette
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
- QAbstractButton, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QScrollArea, QSizePolicy,
- QVBoxLayout, QWidget
+ QAbstractButton, QFrame, QGridLayout, QHBoxLayout, QLabel, QLayout,
+ QScrollArea, QSizePolicy, QVBoxLayout, QWidget
)
from novelwriter import CONFIG
-FONT_SCALE = 0.9
+DEFAULT_SCALE = 0.9
RIGHT_TOP = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop
LEFT_TOP = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop
+class NFixedPage(QFrame):
+ """Extension: Fixed Page Widget
+
+ A custom widget that holds a layout. This is just a wrapper around a
+ QFrame that sets the same frame style as the other Page widgets.
+ """
+
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
+ self.setFrameShadow(QFrame.Shadow.Sunken)
+ self.setFrameShape(QFrame.Shape.StyledPanel)
+ self.setCentralLayout = self.setLayout
+ return
+
+# END Class NFixedPage
+
+
+class NScrollablePage(QScrollArea):
+ """Extension: Scrollable Page Widget
+
+ A custom widget that holds a layout within a scrollable area.
+ """
+
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
+ self._widget = QWidget(self)
+ self.setWidget(self._widget)
+ self.setWidgetResizable(True)
+ self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
+ self.setFrameShadow(QFrame.Shadow.Sunken)
+ self.setFrameShape(QFrame.Shape.StyledPanel)
+ return
+
+ def setCentralLayout(self, layout: QLayout) -> None:
+ """Set the central layout of the scroll page."""
+ self._widget.setLayout(layout)
+ return
+
+# END Class NScrollablePage
+
+
class NScrollableForm(QScrollArea):
+ """Extension: Scrollable Form Widget
+
+ A custom widget that creates a form within a scrollable area.
+ """
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
self._helpCol = QColor(0, 0, 0)
- self._fontScale = FONT_SCALE
+ self._fontScale = DEFAULT_SCALE
self._first = True
+ self._indent = CONFIG.pxInt(12)
self._sections: dict[int, QLabel] = {}
- self._editable: dict[str, NHelpLabel] = {}
+ self._editable: dict[str, NColourLabel] = {}
self._index: dict[str, QWidget] = {}
self._layout = QVBoxLayout()
@@ -61,6 +110,8 @@ class NScrollableForm(QScrollArea):
self.setWidgetResizable(True)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
+ self.setFrameShadow(QFrame.Shadow.Sunken)
+ self.setFrameShape(QFrame.Shape.StyledPanel)
return
@@ -76,9 +127,9 @@ class NScrollableForm(QScrollArea):
# Setters
##
- def setHelpTextStyle(self, color: QColor | list | tuple, scale: float = FONT_SCALE) -> None:
+ def setHelpTextStyle(self, color: QColor, scale: float = DEFAULT_SCALE) -> None:
"""Set the text color for the help text."""
- self._helpCol = color if isinstance(color, QColor) else QColor(*color)
+ self._helpCol = color
self._fontScale = scale
return
@@ -88,6 +139,11 @@ class NScrollableForm(QScrollArea):
qHelp.setText(text)
return
+ def setRowIndent(self, indent: int) -> None:
+ """Set the indentation of each row."""
+ self._indent = max(indent, 0)
+ return
+
##
# Methods
##
@@ -119,37 +175,43 @@ class NScrollableForm(QScrollArea):
return
def addRow(self, label: str, widget: QWidget, helpText: str = "", unit: str | None = None,
- button: QWidget | None = None, editable: str | None = None) -> None:
+ button: QWidget | None = None, editable: str | None = None,
+ stretch: tuple[int, int] = (0, 0)) -> None:
"""Add a label and a widget as a new row of the form."""
row = QHBoxLayout()
- row.setSpacing(CONFIG.pxInt(4))
+ row.setSpacing(CONFIG.pxInt(12))
- mPx = CONFIG.pxInt(12)
qLabel = QLabel(label, self)
- qLabel.setIndent(mPx)
+ qLabel.setIndent(self._indent)
qLabel.setBuddy(widget)
if helpText:
- qHelp = NHelpLabel(str(helpText), self._helpCol, self._fontScale)
- qHelp.setIndent(mPx)
+ qHelp = NColourLabel(
+ str(helpText), self._helpCol, parent=self,
+ scale=self._fontScale, wrap=True, indent=self._indent
+ )
labelBox = QVBoxLayout()
labelBox.addWidget(qLabel)
labelBox.addWidget(qHelp)
labelBox.setSpacing(0)
- labelBox.addStretch(1)
- row.addLayout(labelBox)
+ row.addLayout(labelBox, stretch[0])
if editable:
self._editable[editable] = qHelp
else:
- row.addWidget(qLabel)
-
- row.addSpacing(mPx)
- row.addWidget(widget)
+ row.addWidget(qLabel, stretch[0])
if isinstance(unit, str):
- row.addWidget(QLabel(unit, self))
+ box = QHBoxLayout()
+ box.addWidget(widget)
+ box.addWidget(QLabel(unit, self))
+ row.addLayout(box, stretch[1])
elif isinstance(button, QAbstractButton):
- row.addWidget(button)
+ box = QHBoxLayout()
+ box.addWidget(widget)
+ box.addWidget(button)
+ row.addLayout(box, stretch[1])
+ else:
+ row.addWidget(widget, stretch[1])
self._layout.addLayout(row)
self._index[label.strip()] = widget
@@ -173,7 +235,7 @@ class NConfigLayout(QGridLayout):
self._nextRow = 0
self._helpCol = QColor(0, 0, 0)
- self._fontScale = FONT_SCALE
+ self._fontScale = DEFAULT_SCALE
self._itemMap = {}
wSp = CONFIG.pxInt(8)
@@ -183,24 +245,6 @@ class NConfigLayout(QGridLayout):
return
- ##
- # Getters and Setters
- ##
-
- def setHelpTextStyle(self, color: QColor | list | tuple, scale: float = FONT_SCALE) -> None:
- """Set the text color for the help text."""
- self._helpCol = color if isinstance(color, QColor) else QColor(*color)
- self._fontScale = scale
- return
-
- def setHelpText(self, row: int, text: str) -> None:
- """Set the text for the help label."""
- if row in self._itemMap:
- qHelp = self._itemMap[row][1]
- if isinstance(qHelp, NHelpLabel):
- qHelp.setText(text)
- return
-
##
# Class Methods
##
@@ -216,8 +260,8 @@ class NConfigLayout(QGridLayout):
self._nextRow += 1
return
- def addRow(self, label: str, widget: QWidget, helpText: str | None = None,
- unit: str | None = None, button: QWidget | None = None) -> int:
+ def addRow(self, label: str, widget: QWidget, unit: str | None = None,
+ button: QWidget | None = None) -> int:
"""Add a label and a widget as a new row of the grid."""
wSp = CONFIG.pxInt(8)
qLabel = QLabel(label)
@@ -225,17 +269,7 @@ class NConfigLayout(QGridLayout):
qLabel.setBuddy(widget)
qHelp = None
- if helpText is not None:
- qHelp = NHelpLabel(str(helpText), self._helpCol, self._fontScale)
- qHelp.setIndent(wSp)
- labelBox = QVBoxLayout()
- labelBox.addWidget(qLabel)
- labelBox.addWidget(qHelp)
- labelBox.setSpacing(0)
- labelBox.addStretch(1)
- self.addLayout(labelBox, self._nextRow, 0, 1, 1, LEFT_TOP)
- else:
- self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP)
+ self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP)
if isinstance(unit, str):
controlBox = QHBoxLayout()
@@ -252,12 +286,7 @@ class NConfigLayout(QGridLayout):
self.addLayout(controlBox, self._nextRow, 1, 1, 1, RIGHT_TOP)
else:
- if isinstance(widget, QLineEdit):
- qLayout = QHBoxLayout()
- qLayout.addWidget(widget)
- self.addLayout(qLayout, self._nextRow, 1, 1, 1, RIGHT_TOP)
- else:
- self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP)
+ self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP)
self.setRowStretch(self._nextRow, 0)
self.setRowStretch(self._nextRow+1, 1)
@@ -306,17 +335,9 @@ class NSimpleLayout(QGridLayout):
wSp = CONFIG.pxInt(8)
qLabel = QLabel(label)
qLabel.setIndent(wSp)
- self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP)
-
- if isinstance(widget, QLineEdit):
- qLayout = QHBoxLayout()
- qLayout.addWidget(widget)
- self.addLayout(qLayout, self._nextRow, 1, 1, 1, RIGHT_TOP)
- else:
- self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP)
-
qLabel.setBuddy(widget)
-
+ self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP)
+ self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP)
self.setRowStretch(self._nextRow, 0)
self.setRowStretch(self._nextRow+1, 1)
self._nextRow += 1
@@ -326,25 +347,33 @@ class NSimpleLayout(QGridLayout):
# END Class NSimpleLayout
-class NHelpLabel(QLabel):
+class NColourLabel(QLabel):
+ """Extension: A Coloured Label
- def __init__(self, text: str, color: QColor | list | tuple,
- fontSize: float = FONT_SCALE) -> None:
- super().__init__(text)
+ A custom widget that draws a label in a specific colour, and
+ optionally at a specific size, and word wrapped.
+ """
- qCol = color if isinstance(color, QColor) else QColor(*color)
+ HELP_SCALE = DEFAULT_SCALE
+ HEADER_SCALE = 1.25
- lblCol = self.palette()
- lblCol.setColor(QPalette.WindowText, qCol)
- self.setPalette(lblCol)
+ def __init__(self, text: str, color: QColor, parent: QWidget | None = None,
+ scale: float = HELP_SCALE, wrap: bool = False, indent: int = 0) -> None:
+ super().__init__(text, parent=parent)
- lblFont = self.font()
- lblFont.setPointSizeF(fontSize*lblFont.pointSizeF())
- self.setFont(lblFont)
+ font = self.font()
+ font.setPointSizeF(scale*font.pointSizeF())
+ colour = self.palette()
+ colour.setColor(QPalette.WindowText, color)
- self.setWordWrap(True)
- self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ self.setPalette(colour)
+ self.setFont(font)
+ self.setIndent(indent)
+
+ if wrap:
+ self.setWordWrap(True)
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
return
-# END Class NHelpLabel
+# END Class NColourLabel
diff --git a/novelwriter/extensions/novelselector.py b/novelwriter/extensions/novelselector.py
index 035ab570..44e5bf23 100644
--- a/novelwriter/extensions/novelselector.py
+++ b/novelwriter/extensions/novelselector.py
@@ -3,7 +3,7 @@ novelWriter – Custom Widget: Novel Selector
===========================================
File History:
-Created: 2022-11-17 [2.0]
+Created: 2022-11-17 [2.0] NovelSelector
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
@@ -43,6 +43,8 @@ class NovelSelector(QComboBox):
super().__init__(parent=parent)
self._blockSignal = False
self._firstHandle = None
+ self._includeAll = False
+ self._listFormat = None
self.currentIndexChanged.connect(self._indexChanged)
return
@@ -64,17 +66,29 @@ class NovelSelector(QComboBox):
def setHandle(self, tHandle: str | None, blockSignal: bool = True) -> None:
"""Set the currently selected handle."""
- self._blockSignal = blockSignal
- if tHandle is None:
- index = self.count() - 1
- else:
- index = self.findData(tHandle)
- if index >= 0:
+ if (index := self.findData(tHandle) if tHandle else (self.count() - 1)) >= 0:
+ self._blockSignal = blockSignal
self.setCurrentIndex(index)
- self._blockSignal = False
+ self._blockSignal = False
return
- def updateList(self, includeAll: bool = False, prefix: str | None = None) -> None:
+ def setIncludeAll(self, value: bool) -> None:
+ """Set flag to add an "All Novel Folders" option."""
+ self._includeAll = value
+ return
+
+ def setListFormat(self, value: str | None) -> None:
+ """Set a format string for the list entries."""
+ if value is None or "{0}" in value:
+ self._listFormat = value
+ return
+
+ ##
+ # Public Slots
+ ##
+
+ @pyqtSlot()
+ def refreshNovelList(self) -> None:
"""Rebuild the list of novel items."""
self._blockSignal = True
self._firstHandle = None
@@ -83,8 +97,8 @@ class NovelSelector(QComboBox):
icon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])
handle = self.currentData()
for tHandle, nwItem in SHARED.project.tree.iterRoots(nwItemClass.NOVEL):
- if prefix:
- name = prefix.format(nwItem.itemName)
+ if self._listFormat:
+ name = self._listFormat.format(nwItem.itemName)
self.addItem(name, tHandle)
else:
name = nwItem.itemName
@@ -92,7 +106,7 @@ class NovelSelector(QComboBox):
if self._firstHandle is None:
self._firstHandle = tHandle
- if includeAll:
+ if self._includeAll:
self.insertSeparator(self.count())
self.addItem(icon, self.tr("All Novel Folders"), "")
diff --git a/novelwriter/extensions/pageddialog.py b/novelwriter/extensions/pageddialog.py
deleted file mode 100644
index a620cb74..00000000
--- a/novelwriter/extensions/pageddialog.py
+++ /dev/null
@@ -1,130 +0,0 @@
-"""
-novelWriter – Custom Widget: Paged Dialog
-=========================================
-
-File History:
-Created: 2020-05-17 [0.5.1]
-
-This file is a part of novelWriter
-Copyright 2018–2024, Veronica Berglyd Olsen
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but
-WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-"""
-from __future__ import annotations
-
-from PyQt5.QtGui import QPaintEvent
-from PyQt5.QtCore import QRect, QPoint, QSize
-from PyQt5.QtWidgets import (
- QDialog, QHBoxLayout, QStyle, QStyleOptionTab, QStylePainter, QTabBar,
- QTabWidget, QVBoxLayout, QWidget
-)
-
-from novelwriter import CONFIG
-
-
-class NPagedDialog(QDialog):
-
- def __init__(self, parent: QWidget) -> None:
- super().__init__(parent=parent)
-
- self._tabBar = NVerticalTabBar(self)
- self._tabBar.setExpanding(False)
-
- self._tabBox = QTabWidget(self)
- self._tabBox.setTabBar(self._tabBar)
- self._tabBox.setTabPosition(QTabWidget.West)
-
- self._buttonBox = QHBoxLayout()
-
- self._outerBox = QVBoxLayout()
- self._outerBox.addWidget(self._tabBox)
- self._outerBox.addLayout(self._buttonBox)
-
- # Default Margins
- thisStyle = self.style()
- mL = thisStyle.pixelMetric(QStyle.PM_LayoutLeftMargin)
- mR = thisStyle.pixelMetric(QStyle.PM_LayoutRightMargin)
- mT = thisStyle.pixelMetric(QStyle.PM_LayoutLeftMargin)
- mB = thisStyle.pixelMetric(QStyle.PM_LayoutBottomMargin)
-
- # Set Margins
- self.setContentsMargins(0, 0, 0, 0)
- self._outerBox.setContentsMargins(0, 0, 0, mB)
- self._buttonBox.setContentsMargins(mL, 0, mR, 0)
- self._outerBox.setSpacing(mT)
-
- self.setLayout(self._outerBox)
-
- return
-
- def addTab(self, widget: QWidget, label: str) -> None:
- """Forward the adding of tabs to the QTabWidget."""
- self._tabBox.addTab(widget, label)
- return
-
- def addControls(self, buttonBar: QWidget) -> None:
- """Add a button bar to the dialog."""
- self._buttonBox.addWidget(buttonBar)
- return
-
- def setCurrentWidget(self, widget: QWidget) -> None:
- """Forward the changing of tab to the QTabWidget."""
- self._tabBox.setCurrentWidget(widget)
- return
-
-# END Class NPagedDialog
-
-
-class NVerticalTabBar(QTabBar):
-
- def __init__(self, parent: QWidget) -> None:
- super().__init__(parent=parent)
- self._mW = CONFIG.pxInt(150)
- return
-
- def tabSizeHint(self, index: int) -> QSize:
- """Return a transposed size hint for the rotated bar."""
- tSize = super().tabSizeHint(index)
- tSize.transpose()
- tSize.setWidth(min(tSize.width(), self._mW))
- return tSize
-
- def paintEvent(self, event: QPaintEvent) -> None:
- """Custom implementation of the label painter that rotates the
- label 90 degrees.
- """
- pObj = QStylePainter(self)
- oObj = QStyleOptionTab()
-
- for i in range(self.count()):
- self.initStyleOption(oObj, i)
- pObj.drawControl(QStyle.CE_TabBarTabShape, oObj)
- pObj.save()
-
- oSize = oObj.rect.size()
- oSize.transpose()
- oRect = QRect(QPoint(), oSize)
- oRect.moveCenter(oObj.rect.center())
- oObj.rect = oRect
-
- oCenter = self.tabRect(i).center()
- pObj.translate(oCenter)
- pObj.rotate(90)
- pObj.translate(-oCenter)
- pObj.drawControl(QStyle.CE_TabBarTabLabel, oObj)
- pObj.restore()
-
- return
-
-# END Class NVerticalTabBar
diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py
index 2f417b44..082e8a2a 100644
--- a/novelwriter/extensions/pagedsidebar.py
+++ b/novelwriter/extensions/pagedsidebar.py
@@ -45,10 +45,9 @@ class NPagedSideBar(QToolBar):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
- self._buttons = []
- self._actions = []
self._labelCol = None
self._spacerHeight = self.fontMetrics().height() // 2
+ self._buttons: dict[int, _NPagedToolButton] = {}
self._group = QButtonGroup(self)
self._group.setExclusive(True)
@@ -67,9 +66,9 @@ class NPagedSideBar(QToolBar):
"""Return a specific button."""
return self._buttons[buttonId]
- def setLabelColor(self, color: list | QColor) -> None:
+ def setLabelColor(self, color: QColor) -> None:
"""Set the text color for the labels."""
- self._labelCol = color if isinstance(color, QColor) else QColor(*color)
+ self._labelCol = color
return
def addSeparator(self) -> None:
@@ -95,8 +94,7 @@ class NPagedSideBar(QToolBar):
action = self.insertWidget(self._stretchAction, button)
self._group.addButton(button, id=buttonId)
- self._buttons.append(button)
- self._actions.append(action)
+ self._buttons[buttonId] = button
return action
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index c7554591..6baddf67 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -276,14 +276,14 @@ class GuiDocEditor(QPlainTextEdit):
def updateSyntaxColours(self) -> None:
"""Update the syntax highlighting theme."""
mainPalette = self.palette()
- mainPalette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
- mainPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack))
- mainPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
+ mainPalette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
+ mainPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
+ mainPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(mainPalette)
docPalette = self.viewport().palette()
- docPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack))
- docPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
+ docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
+ docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.viewport().setPalette(docPalette)
self.docHeader.matchColours()
@@ -2326,9 +2326,9 @@ class GuiDocToolBar(QWidget):
def updateTheme(self) -> None:
"""Initialise GUI elements that depend on specific settings."""
palette = QPalette()
- palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
- palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
- palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
+ palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
+ palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
+ palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(palette)
self.tbBoldMD.setIcon(SHARED.theme.getIcon("fmt_bold-md"))
@@ -2866,10 +2866,11 @@ class GuiDocEditHeader(QWidget):
self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
self.closeButton.setIcon(SHARED.theme.getIcon("close"))
+ colText = SHARED.theme.colText
buttonStyle = (
"QToolButton {{border: none; background: transparent;}} "
- "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
- ).format(*SHARED.theme.colText)
+ "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
+ ).format(colText.red(), colText.green(), colText.blue())
self.tbButton.setStyleSheet(buttonStyle)
self.searchButton.setStyleSheet(buttonStyle)
@@ -2885,9 +2886,9 @@ class GuiDocEditHeader(QWidget):
theme rather than the main GUI.
"""
palette = QPalette()
- palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
- palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
- palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
+ palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
+ palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
+ palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(palette)
self.itemTitle.setPalette(palette)
@@ -3093,9 +3094,9 @@ class GuiDocEditFooter(QWidget):
theme rather than the main GUI.
"""
palette = QPalette()
- palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
- palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
- palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
+ palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
+ palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
+ palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(palette)
self.statusText.setPalette(palette)
diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py
index 5dcece0f..3b4dc684 100644
--- a/novelwriter/gui/dochighlight.py
+++ b/novelwriter/gui/dochighlight.py
@@ -48,6 +48,8 @@ SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
class GuiDocHighlighter(QSyntaxHighlighter):
+ __slots__ = ("_tItem", "_tHandle", "_spellCheck", "_spellErr", "_hRules", "_hStyles")
+
BLOCK_NONE = 0
BLOCK_TEXT = 1
BLOCK_META = 2
@@ -58,29 +60,14 @@ class GuiDocHighlighter(QSyntaxHighlighter):
logger.debug("Create: GuiDocHighlighter")
- self._tItem = None
- self._tHandle = None
+ self._tItem = None
+ self._tHandle = None
self._spellCheck = False
+ self._spellErr = QTextCharFormat()
self._hRules: list[tuple[str, dict]] = []
self._hStyles: dict[str, QTextCharFormat] = {}
- self._colHead = QColor(0, 0, 0)
- self._colHeadH = QColor(0, 0, 0)
- self._colEmph = QColor(0, 0, 0)
- self._colDialN = QColor(0, 0, 0)
- self._colDialD = QColor(0, 0, 0)
- self._colDialS = QColor(0, 0, 0)
- self._colHidden = QColor(0, 0, 0)
- self._colCode = QColor(0, 0, 0)
- self._colKey = QColor(0, 0, 0)
- self._colVal = QColor(0, 0, 0)
- self._colSpell = QColor(0, 0, 0)
- self._colError = QColor(0, 0, 0)
- self._colRepTag = QColor(0, 0, 0)
- self._colMod = QColor(0, 0, 0)
- self._colBreak = QColor(0, 0, 0)
-
self.initHighlighter()
logger.debug("Ready: GuiDocHighlighter")
@@ -93,54 +80,42 @@ class GuiDocHighlighter(QSyntaxHighlighter):
"""
logger.debug("Setting up highlighting rules")
- self._colHead = QColor(*SHARED.theme.colHead)
- self._colHeadH = QColor(*SHARED.theme.colHeadH)
- self._colDialN = QColor(*SHARED.theme.colDialN)
- self._colDialD = QColor(*SHARED.theme.colDialD)
- self._colDialS = QColor(*SHARED.theme.colDialS)
- self._colHidden = QColor(*SHARED.theme.colHidden)
- self._colCode = QColor(*SHARED.theme.colCode)
- self._colKey = QColor(*SHARED.theme.colKey)
- self._colVal = QColor(*SHARED.theme.colVal)
- self._colSpell = QColor(*SHARED.theme.colSpell)
- self._colError = QColor(*SHARED.theme.colError)
- self._colRepTag = QColor(*SHARED.theme.colRepTag)
- self._colMod = QColor(*SHARED.theme.colMod)
- self._colBreak = QColor(*SHARED.theme.colEmph)
- self._colBreak.setAlpha(64)
-
- self._colEmph = None
- if CONFIG.highlightEmph:
- self._colEmph = QColor(*SHARED.theme.colEmph)
+ colEmph = SHARED.theme.colEmph if CONFIG.highlightEmph else None
+ colBreak = QColor(SHARED.theme.colEmph)
+ colBreak.setAlpha(64)
+ self._hRules = []
self._hStyles = {
- "header1": self._makeFormat(self._colHead, "bold", 1.8),
- "header2": self._makeFormat(self._colHead, "bold", 1.6),
- "header3": self._makeFormat(self._colHead, "bold", 1.4),
- "header4": self._makeFormat(self._colHead, "bold", 1.2),
- "header1h": self._makeFormat(self._colHeadH, "bold", 1.8),
- "header2h": self._makeFormat(self._colHeadH, "bold", 1.6),
- "header3h": self._makeFormat(self._colHeadH, "bold", 1.4),
- "header4h": self._makeFormat(self._colHeadH, "bold", 1.2),
- "bold": self._makeFormat(self._colEmph, "bold"),
- "italic": self._makeFormat(self._colEmph, "italic"),
- "strike": self._makeFormat(self._colHidden, "strike"),
- "mspaces": self._makeFormat(self._colError, "errline"),
- "nobreak": self._makeFormat(self._colBreak, "background"),
- "dialogue1": self._makeFormat(self._colDialN),
- "dialogue2": self._makeFormat(self._colDialD),
- "dialogue3": self._makeFormat(self._colDialS),
- "replace": self._makeFormat(self._colRepTag),
- "hidden": self._makeFormat(self._colHidden),
- "code": self._makeFormat(self._colCode),
- "keyword": self._makeFormat(self._colKey),
- "modifier": self._makeFormat(self._colMod),
- "value": self._makeFormat(self._colVal, "underline"),
- "codevalue": self._makeFormat(self._colVal),
+ "header1": self._makeFormat(SHARED.theme.colHead, "bold", 1.8),
+ "header2": self._makeFormat(SHARED.theme.colHead, "bold", 1.6),
+ "header3": self._makeFormat(SHARED.theme.colHead, "bold", 1.4),
+ "header4": self._makeFormat(SHARED.theme.colHead, "bold", 1.2),
+ "header1h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.8),
+ "header2h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.6),
+ "header3h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.4),
+ "header4h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.2),
+ "bold": self._makeFormat(colEmph, "bold"),
+ "italic": self._makeFormat(colEmph, "italic"),
+ "strike": self._makeFormat(SHARED.theme.colHidden, "strike"),
+ "mspaces": self._makeFormat(SHARED.theme.colError, "errline"),
+ "nobreak": self._makeFormat(colBreak, "background"),
+ "dialogue1": self._makeFormat(SHARED.theme.colDialN),
+ "dialogue2": self._makeFormat(SHARED.theme.colDialD),
+ "dialogue3": self._makeFormat(SHARED.theme.colDialS),
+ "replace": self._makeFormat(SHARED.theme.colRepTag),
+ "hidden": self._makeFormat(SHARED.theme.colHidden),
+ "code": self._makeFormat(SHARED.theme.colCode),
+ "keyword": self._makeFormat(SHARED.theme.colKey),
+ "modifier": self._makeFormat(SHARED.theme.colMod),
+ "value": self._makeFormat(SHARED.theme.colVal, "underline"),
+ "codevalue": self._makeFormat(SHARED.theme.colVal),
"codeinval": self._makeFormat(None, "errline"),
}
- self._hRules = []
+ # Cache Spell Error Format
+ self._spellErr = QTextCharFormat()
+ self._spellErr.setUnderlineColor(SHARED.theme.colSpell)
+ self._spellErr.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
# Multiple or Trailing Spaces
if CONFIG.showMultiSpaces:
@@ -305,12 +280,12 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setCurrentBlockState(self.BLOCK_META)
if self._tItem:
pIndex = SHARED.project.index
- isValid, theBits, thePos = pIndex.scanThis(text)
- isGood = pIndex.checkThese(theBits, self._tItem)
+ isValid, bits, pos = pIndex.scanThis(text)
+ isGood = pIndex.checkThese(bits, self._tItem)
if isValid:
- for n, theBit in enumerate(theBits):
- xPos = thePos[n]
- xLen = len(theBit)
+ for n, bit in enumerate(bits):
+ xPos = pos[n]
+ xLen = len(bit)
if isGood[n]:
if n == 0:
self.setFormat(xPos, xLen, self._hStyles["keyword"])
@@ -318,8 +293,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setFormat(xPos, xLen, self._hStyles["value"])
else:
kwFmt = self.format(xPos)
- kwFmt.setUnderlineColor(self._colError)
- kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
+ kwFmt.merge(self._hStyles["codeinval"])
self.setFormat(xPos, xLen, kwFmt)
# We never want to run the spell checker on keyword/values,
@@ -402,8 +376,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
for xPos, xLen in data.spellCheck(text):
for x in range(xPos, xPos+xLen):
spFmt = self.format(x)
- spFmt.setUnderlineColor(self._colSpell)
- spFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
+ spFmt.merge(self._spellErr)
self.setFormat(x, 1, spFmt)
return
@@ -431,7 +404,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
if "strike" in styles:
charFormat.setFontStrikeOut(True)
if "errline" in styles:
- charFormat.setUnderlineColor(self._colError)
+ charFormat.setUnderlineColor(SHARED.theme.colError)
charFormat.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
if "underline" in styles:
charFormat.setFontUnderline(True)
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 5a066ccc..3a567668 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -33,7 +33,7 @@ from typing import TYPE_CHECKING
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QPoint, QSize, Qt, QUrl
from PyQt5.QtGui import (
- QColor, QCursor, QFont, QMouseEvent, QPalette, QResizeEvent, QTextCursor,
+ QCursor, QFont, QMouseEvent, QPalette, QResizeEvent, QTextCursor,
QTextOption
)
from PyQt5.QtWidgets import (
@@ -149,14 +149,14 @@ class GuiDocViewer(QTextBrowser):
# Set the widget colours to match syntax theme
mainPalette = self.palette()
- mainPalette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
- mainPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack))
- mainPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
+ mainPalette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
+ mainPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
+ mainPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(mainPalette)
docPalette = self.viewport().palette()
- docPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack))
- docPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
+ docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
+ docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.viewport().setPalette(docPalette)
self.docHeader.matchColours()
@@ -463,7 +463,13 @@ class GuiDocViewer(QTextBrowser):
"""Generate an appropriate style sheet for the document viewer,
based on the current syntax highlighter theme,
"""
- pTheme = SHARED.theme
+ colText = SHARED.theme.colText
+ colHead = SHARED.theme.colHead
+ colVal = SHARED.theme.colVal
+ colEmph = SHARED.theme.colEmph
+ colKey = SHARED.theme.colKey
+ colHidden = SHARED.theme.colHidden
+ colMod = SHARED.theme.colMod
styleSheet = (
"body {{"
" color: rgb({tColR}, {tColG}, {tColB});"
@@ -490,27 +496,27 @@ class GuiDocViewer(QTextBrowser):
" text-align: center;"
"}}\n"
).format(
- tColR=pTheme.colText[0],
- tColG=pTheme.colText[1],
- tColB=pTheme.colText[2],
- hColR=pTheme.colHead[0],
- hColG=pTheme.colHead[1],
- hColB=pTheme.colHead[2],
- aColR=pTheme.colVal[0],
- aColG=pTheme.colVal[1],
- aColB=pTheme.colVal[2],
- eColR=pTheme.colEmph[0],
- eColG=pTheme.colEmph[1],
- eColB=pTheme.colEmph[2],
- kColR=pTheme.colKey[0],
- kColG=pTheme.colKey[1],
- kColB=pTheme.colKey[2],
- cColR=pTheme.colHidden[0],
- cColG=pTheme.colHidden[1],
- cColB=pTheme.colHidden[2],
- mColR=pTheme.colMod[0],
- mColG=pTheme.colMod[1],
- mColB=pTheme.colMod[2],
+ tColR=colText.red(),
+ tColG=colText.green(),
+ tColB=colText.blue(),
+ hColR=colHead.red(),
+ hColG=colHead.green(),
+ hColB=colHead.blue(),
+ aColR=colVal.red(),
+ aColG=colVal.green(),
+ aColB=colVal.blue(),
+ eColR=colEmph.red(),
+ eColG=colEmph.green(),
+ eColB=colEmph.blue(),
+ kColR=colKey.red(),
+ kColG=colKey.green(),
+ kColB=colKey.blue(),
+ cColR=colHidden.red(),
+ cColG=colHidden.green(),
+ cColB=colHidden.blue(),
+ mColR=colMod.red(),
+ mColG=colMod.green(),
+ mColB=colMod.blue(),
)
self.document().setDefaultStyleSheet(styleSheet)
@@ -744,10 +750,11 @@ class GuiDocViewHeader(QWidget):
self.refreshButton.setIcon(SHARED.theme.getIcon("refresh"))
self.closeButton.setIcon(SHARED.theme.getIcon("close"))
+ colText = SHARED.theme.colText
buttonStyle = (
"QToolButton {{border: none; background: transparent;}} "
- "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
- ).format(*SHARED.theme.colText)
+ "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
+ ).format(colText.red(), colText.green(), colText.blue())
self.backButton.setStyleSheet(buttonStyle)
self.forwardButton.setStyleSheet(buttonStyle)
@@ -763,9 +770,9 @@ class GuiDocViewHeader(QWidget):
theme rather than the main GUI.
"""
palette = QPalette()
- palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
- palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
- palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
+ palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
+ palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
+ palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(palette)
self.docTitle.setPalette(palette)
return
@@ -937,11 +944,11 @@ class GuiDocViewFooter(QWidget):
self.showComments.setIcon(bulletIcon)
self.showSynopsis.setIcon(bulletIcon)
- # StyleSheets
+ colText = SHARED.theme.colText
buttonStyle = (
"QToolButton {{border: none; background: transparent;}} "
- "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
- ).format(*SHARED.theme.colText)
+ "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
+ ).format(colText.red(), colText.green(), colText.blue())
self.showHide.setStyleSheet(buttonStyle)
self.showComments.setStyleSheet(buttonStyle)
@@ -956,9 +963,9 @@ class GuiDocViewFooter(QWidget):
theme rather than the main GUI.
"""
palette = QPalette()
- palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
- palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
- palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
+ palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
+ palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
+ palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(palette)
return
diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py
index 189cd72d..279fce07 100644
--- a/novelwriter/gui/mainmenu.py
+++ b/novelwriter/gui/mainmenu.py
@@ -155,10 +155,10 @@ class GuiMainMenu(QMenuBar):
self.aProjectSettings.setShortcut("Ctrl+Shift+,")
self.aProjectSettings.triggered.connect(self.mainGui.showProjectSettingsDialog)
- # Project > Project Details
- self.aProjectDetails = self.projMenu.addAction(self.tr("Project Details"))
- self.aProjectDetails.setShortcut("Shift+F6")
- self.aProjectDetails.triggered.connect(self.mainGui.showProjectDetailsDialog)
+ # Project > Novel Details
+ self.aNovelDetails = self.projMenu.addAction(self.tr("Novel Details"))
+ self.aNovelDetails.setShortcut("Shift+F6")
+ self.aNovelDetails.triggered.connect(self.mainGui.showNovelDetailsDialog)
# Project > Separator
self.projMenu.addSeparator()
diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py
index 1569262b..959ec7ae 100644
--- a/novelwriter/gui/noveltree.py
+++ b/novelwriter/gui/noveltree.py
@@ -207,9 +207,10 @@ class GuiNovelToolBar(QWidget):
# Novel Selector
selFont = self.font()
selFont.setWeight(QFont.Weight.Bold)
- self.novelPrefix = self.tr("Outline of {0}")
+
self.novelValue = NovelSelector(self)
self.novelValue.setFont(selFont)
+ self.novelValue.setListFormat(self.tr("Outline of {0}"))
self.novelValue.setMinimumWidth(CONFIG.pxInt(150))
self.novelValue.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.novelValue.novelSelectionChanged.connect(self.setCurrentRoot)
@@ -294,7 +295,7 @@ class GuiNovelToolBar(QWidget):
"QComboBox {border-style: none; padding-left: 0;} "
"QComboBox::drop-down {border-style: none}"
)
- self.novelValue.updateList(prefix=self.novelPrefix)
+ self.novelValue.refreshNovelList()
self.tbNovel.setVisible(self.novelValue.count() > 1)
return
@@ -307,7 +308,7 @@ class GuiNovelToolBar(QWidget):
def buildNovelRootMenu(self) -> None:
"""Build the novel root menu."""
- self.novelValue.updateList(prefix=self.novelPrefix)
+ self.novelValue.refreshNovelList()
self.tbNovel.setVisible(self.novelValue.count() > 1)
return
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index db066cee..cd441bae 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -220,6 +220,7 @@ class GuiOutlineToolBar(QToolBar):
self.novelLabel.setContentsMargins(0, 0, mPx, 0)
self.novelValue = NovelSelector(self)
+ self.novelValue.setIncludeAll(True)
self.novelValue.setMinimumWidth(CONFIG.pxInt(200))
self.novelValue.novelSelectionChanged.connect(self._novelValueChanged)
@@ -258,7 +259,7 @@ class GuiOutlineToolBar(QToolBar):
def updateTheme(self) -> None:
"""Update theme elements."""
self.setStyleSheet("QToolBar {border: 0px;}")
- self.novelValue.updateList(includeAll=True)
+ self.novelValue.refreshNovelList()
self.aRefresh.setIcon(SHARED.theme.getIcon("refresh"))
self.tbColumns.setIcon(SHARED.theme.getIcon("menu"))
self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}")
@@ -266,7 +267,7 @@ class GuiOutlineToolBar(QToolBar):
def populateNovelList(self) -> None:
"""Reload the content of the novel list."""
- self.novelValue.updateList(includeAll=True)
+ self.novelValue.refreshNovelList()
return
def setCurrentRoot(self, rootHandle: str | None) -> None:
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index 8c42a107..da050876 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -48,7 +48,7 @@ from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter
from novelwriter.dialogs.docmerge import GuiDocMerge
from novelwriter.dialogs.docsplit import GuiDocSplit
from novelwriter.dialogs.editlabel import GuiEditLabel
-from novelwriter.dialogs.projsettings import GuiProjectSettings
+from novelwriter.dialogs.projectsettings import GuiProjectSettings
from novelwriter.enum import (
nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwWidget
)
@@ -1748,7 +1748,7 @@ class _TreeContextMenu(QMenu):
menu.addSeparator()
action = menu.addAction(self.tr("Manage Labels ..."))
action.triggered.connect(
- lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_STATUS)
+ lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_STATUS)
)
else:
menu = self.addMenu(self.tr("Set Importance to ..."))
@@ -1765,7 +1765,7 @@ class _TreeContextMenu(QMenu):
menu.addSeparator()
action = menu.addAction(self.tr("Manage Labels ..."))
action.triggered.connect(
- lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_IMPORT)
+ lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_IMPORT)
)
return
diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py
index 92522542..d425ccf8 100644
--- a/novelwriter/gui/sidebar.py
+++ b/novelwriter/gui/sidebar.py
@@ -79,9 +79,9 @@ class GuiSideBar(QWidget):
self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
self.tbDetails = QToolButton(self)
- self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Project Details")))
+ self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details")))
self.tbDetails.setIconSize(iconSize)
- self.tbDetails.clicked.connect(self.mainGui.showProjectDetailsDialog)
+ self.tbDetails.clicked.connect(self.mainGui.showNovelDetailsDialog)
self.tbStats = QToolButton(self)
self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics")))
diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py
index fd95be74..61168f20 100644
--- a/novelwriter/gui/statusbar.py
+++ b/novelwriter/gui/statusbar.py
@@ -29,7 +29,6 @@ from time import time
from typing import TYPE_CHECKING, Literal
from datetime import datetime
-from PyQt5.QtGui import QColor
from PyQt5.QtCore import pyqtSlot, QLocale
from PyQt5.QtWidgets import qApp, QStatusBar, QLabel
@@ -55,9 +54,9 @@ class GuiMainStatus(QStatusBar):
self._userIdle = False
self._debugInfo = False
- colNone = QColor(*SHARED.theme.statNone)
- colSaved = QColor(*SHARED.theme.statSaved)
- colUnsaved = QColor(*SHARED.theme.statUnsaved)
+ colNone = SHARED.theme.statNone
+ colSaved = SHARED.theme.statSaved
+ colUnsaved = SHARED.theme.statUnsaved
iPx = SHARED.theme.baseIconSize
diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py
index 2e3e8b94..5f8dbfac 100644
--- a/novelwriter/gui/theme.py
+++ b/novelwriter/gui/theme.py
@@ -70,10 +70,10 @@ class GuiTheme:
self.isLightTheme = True
# GUI
- self.statNone = [120, 120, 120]
- self.statUnsaved = [200, 15, 39]
- self.statSaved = [2, 133, 37]
- self.helpText = [0, 0, 0]
+ self.statNone = QColor(120, 120, 120)
+ self.statUnsaved = QColor(200, 15, 39)
+ self.statSaved = QColor(2, 133, 37)
+ self.helpText = QColor(0, 0, 0)
# Loaded Syntax Settings
# ======================
@@ -88,23 +88,23 @@ class GuiTheme:
self.syntaxLicenseUrl = ""
# Colours
- self.colBack = [255, 255, 255]
- self.colText = [0, 0, 0]
- self.colLink = [0, 0, 0]
- self.colHead = [0, 0, 0]
- self.colHeadH = [0, 0, 0]
- self.colEmph = [0, 0, 0]
- self.colDialN = [0, 0, 0]
- self.colDialD = [0, 0, 0]
- self.colDialS = [0, 0, 0]
- self.colHidden = [0, 0, 0]
- self.colCode = [0, 0, 0]
- self.colKey = [0, 0, 0]
- self.colVal = [0, 0, 0]
- self.colSpell = [0, 0, 0]
- self.colError = [0, 0, 0]
- self.colRepTag = [0, 0, 0]
- self.colMod = [0, 0, 0]
+ self.colBack = QColor(255, 255, 255)
+ self.colText = QColor(0, 0, 0)
+ self.colLink = QColor(0, 0, 0)
+ self.colHead = QColor(0, 0, 0)
+ self.colHeadH = QColor(0, 0, 0)
+ self.colEmph = QColor(0, 0, 0)
+ self.colDialN = QColor(0, 0, 0)
+ self.colDialD = QColor(0, 0, 0)
+ self.colDialS = QColor(0, 0, 0)
+ self.colHidden = QColor(0, 0, 0)
+ self.colCode = QColor(0, 0, 0)
+ self.colKey = QColor(0, 0, 0)
+ self.colVal = QColor(0, 0, 0)
+ self.colSpell = QColor(0, 0, 0)
+ self.colError = QColor(0, 0, 0)
+ self.colRepTag = QColor(0, 0, 0)
+ self.colMod = QColor(0, 0, 0)
# Class Setup
# ===========
@@ -256,12 +256,12 @@ class GuiTheme:
backLNess = backCol.lightnessF()
textLNess = textCol.lightnessF()
self.isLightTheme = backLNess > textLNess
- if self.helpText == [0, 0, 0]:
+ if self.helpText == QColor(0, 0, 0):
if self.isLightTheme:
helpLCol = textLNess + 0.35*(backLNess - textLNess)
else:
helpLCol = backLNess + 0.65*(textLNess - backLNess)
- self.helpText = [int(255*helpLCol)]*3
+ self.helpText = QColor.fromHsl(0, 0, int(255*helpLCol))
# Icons
defaultIcons = "typicons_light" if backLNess >= 0.5 else "typicons_dark"
@@ -398,29 +398,14 @@ class GuiTheme:
return True
- def _parseColour(self, parser: NWConfigParser, section: str, name: str) -> list[int]:
+ def _parseColour(self, parser: NWConfigParser, section: str, name: str) -> QColor:
"""Parse a colour value from a config string."""
- if parser.has_option(section, name):
- values = parser.get(section, name).split(",")
- result = []
- try:
- result.append(minmax(int(values[0]), 0, 255))
- result.append(minmax(int(values[1]), 0, 255))
- result.append(minmax(int(values[2]), 0, 255))
- except Exception:
- logger.error("Could not load theme colours for '%s' from config file", name)
- result = [0, 0, 0]
- else:
- logger.warning("Could not find theme colours for '%s' in config file", name)
- result = [0, 0, 0]
- return result
+ return QColor(*parser.rdIntList(section, name, [0, 0, 0, 255]))
def _setPalette(self, parser: NWConfigParser, section: str,
name: str, value: QPalette.ColorRole) -> None:
"""Set a palette colour value from a config string."""
- self._guiPalette.setColor(
- value, QColor(*self._parseColour(parser, section, name))
- )
+ self._guiPalette.setColor(value, self._parseColour(parser, section, name))
return
# End Class GuiTheme
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 6717351a..0282b8d7 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -53,11 +53,11 @@ from novelwriter.dialogs.about import GuiAbout
from novelwriter.dialogs.updates import GuiUpdates
from novelwriter.dialogs.wordlist import GuiWordList
from novelwriter.dialogs.preferences import GuiPreferences
-from novelwriter.dialogs.projdetails import GuiProjectDetails
-from novelwriter.dialogs.projsettings import GuiProjectSettings
+from novelwriter.dialogs.projectsettings import GuiProjectSettings
from novelwriter.tools.welcome import GuiWelcome
from novelwriter.tools.manuscript import GuiManuscript
from novelwriter.tools.dictionaries import GuiDictionaries
+from novelwriter.tools.noveldetails import GuiNovelDetails
from novelwriter.tools.writingstats import GuiWritingStats
from novelwriter.enum import (
@@ -79,13 +79,6 @@ class GuiMain(QMainWindow):
function. Also, the project instance and theme instance are created
here. These should be passed around to all other objects who need
them and new instances of them should generally not be created.
-
- * All other GUI classes that depend on any components from the
- main GUI should be passed a reference to the instance of this
- class.
- * All non-GUI classes can be passed a reference to the NWProject
- instance if the Main GUI is not needed (which it generally
- shouldn't need).
"""
def __init__(self) -> None:
@@ -816,19 +809,19 @@ class GuiMain(QMainWindow):
@pyqtSlot()
@pyqtSlot(int)
- def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.TAB_MAIN) -> None:
+ def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.PAGE_SETTINGS) -> None:
"""Open the project settings dialog."""
if SHARED.hasProject:
- dialog = GuiProjectSettings(self, focusTab=focusTab)
+ dialog = GuiProjectSettings(self, gotoPage=focusTab)
dialog.newProjectSettingsReady.connect(self._processProjectSettingsChanges)
dialog.exec_()
return
@pyqtSlot()
- def showProjectDetailsDialog(self) -> None:
- """Open the project details dialog."""
+ def showNovelDetailsDialog(self) -> None:
+ """Open the novel details dialog."""
if SHARED.hasProject:
- dialog = GuiProjectDetails(self)
+ dialog = GuiNovelDetails(self)
dialog.setModal(True)
dialog.show()
dialog.raise_()
@@ -1118,13 +1111,15 @@ class GuiMain(QMainWindow):
return
- @pyqtSlot()
- def _processProjectSettingsChanges(self) -> None:
+ @pyqtSlot(bool)
+ def _processProjectSettingsChanges(self, rebuildTrees: bool) -> None:
"""Refresh data dependent on project settings."""
logger.debug("Applying new project settings")
SHARED.updateSpellCheckLanguage()
self.itemDetails.refreshDetails()
self._updateWindowTitle(SHARED.project.data.name)
+ if rebuildTrees:
+ self.rebuildTrees()
return
@pyqtSlot()
diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py
index fb0324d1..e0c034af 100644
--- a/novelwriter/tools/manussettings.py
+++ b/novelwriter/tools/manussettings.py
@@ -27,9 +27,7 @@ import logging
from typing import TYPE_CHECKING
-from PyQt5.QtGui import (
- QColor, QFont, QIcon, QSyntaxHighlighter, QTextCharFormat, QTextDocument
-)
+from PyQt5.QtGui import QFont, QIcon, QSyntaxHighlighter, QTextCharFormat, QTextDocument
from PyQt5.QtCore import QEvent, QSize, Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QAbstractButton, QAbstractItemView, QComboBox, QDialog, QDialogButtonBox,
@@ -862,9 +860,9 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter):
def __init__(self, document: QTextDocument) -> None:
super().__init__(document)
self._fmtSymbol = QTextCharFormat()
- self._fmtSymbol.setForeground(QColor(*SHARED.theme.colHead))
+ self._fmtSymbol.setForeground(SHARED.theme.colHead)
self._fmtFormat = QTextCharFormat()
- self._fmtFormat.setForeground(QColor(*SHARED.theme.colEmph))
+ self._fmtFormat.setForeground(SHARED.theme.colEmph)
return
def highlightBlock(self, text: str) -> None:
diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py
new file mode 100644
index 00000000..71375d7f
--- /dev/null
+++ b/novelwriter/tools/noveldetails.py
@@ -0,0 +1,525 @@
+"""
+novelWriter – GUI Novel Info
+============================
+
+File History:
+Created: 2024-01-18 [2.3b1] GuiNovelDetails
+
+This file is a part of novelWriter
+Copyright 2018–2024, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+from __future__ import annotations
+
+import math
+import logging
+
+from PyQt5.QtGui import QCloseEvent
+from PyQt5.QtCore import QSize, Qt, pyqtSlot
+from PyQt5.QtWidgets import (
+ QAbstractItemView, QDialog, QDialogButtonBox, QFormLayout, QGridLayout,
+ QHBoxLayout, QLabel, QSpinBox, QStackedWidget, QTreeWidget,
+ QTreeWidgetItem, QVBoxLayout, QWidget
+)
+
+from novelwriter import CONFIG, SHARED
+from novelwriter.common import formatTime, numberToRoman
+from novelwriter.constants import nwUnicode
+from novelwriter.extensions.switch import NSwitch
+from novelwriter.extensions.configlayout import NColourLabel, NFixedPage, NScrollablePage
+from novelwriter.extensions.pagedsidebar import NPagedSideBar
+from novelwriter.extensions.novelselector import NovelSelector
+
+logger = logging.getLogger(__name__)
+
+
+class GuiNovelDetails(QDialog):
+
+ PAGE_OVERVIEW = 1
+ PAGE_CONTENTS = 2
+
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
+
+ logger.debug("Create: GuiNovelDetails")
+ self.setObjectName("GuiNovelDetails")
+ self.setWindowTitle(self.tr("Novel Details"))
+
+ options = SHARED.project.options
+ self.setMinimumSize(CONFIG.pxInt(500), CONFIG.pxInt(400))
+ self.resize(
+ CONFIG.pxInt(options.getInt("GuiNovelDetails", "winWidth", CONFIG.pxInt(650))),
+ CONFIG.pxInt(options.getInt("GuiNovelDetails", "winHeight", CONFIG.pxInt(500)))
+ )
+
+ # Title
+ self.titleLabel = NColourLabel(
+ self.tr("Novel Details"), SHARED.theme.helpText,
+ parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
+ )
+
+ # Novel Selector
+ self.novelSelector = NovelSelector(self)
+ self.novelSelector.refreshNovelList()
+ self.novelSelector.setHandle(
+ options.getString("GuiNovelDetails", "novelRoot", self.novelSelector.firstHandle or "")
+ )
+
+ # SideBar
+ self.sidebar = NPagedSideBar(self)
+ self.sidebar.setLabelColor(SHARED.theme.helpText)
+ self.sidebar.addButton(self.tr("Overview"), self.PAGE_OVERVIEW)
+ self.sidebar.addButton(self.tr("Contents"), self.PAGE_CONTENTS)
+ self.sidebar.setSelected(self.PAGE_OVERVIEW)
+ self.sidebar.buttonClicked.connect(self._sidebarClicked)
+
+ # Content
+ self.overviewPage = _OverviewPage(self)
+ self.contentsPage = _ContentsPage(self)
+
+ self.mainStack = QStackedWidget(self)
+ self.mainStack.addWidget(self.overviewPage)
+ self.mainStack.addWidget(self.contentsPage)
+
+ # Buttons
+ self.buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
+ self.buttonBox.rejected.connect(self.close)
+
+ # Assemble
+ self.topBox = QHBoxLayout()
+ self.topBox.addWidget(self.titleLabel)
+ self.topBox.addStretch(1)
+ self.topBox.addWidget(self.novelSelector, 1)
+
+ self.mainBox = QHBoxLayout()
+ self.mainBox.addWidget(self.sidebar)
+ self.mainBox.addWidget(self.mainStack)
+ self.mainBox.setContentsMargins(0, 0, 0, 0)
+
+ self.outerBox = QVBoxLayout()
+ self.outerBox.addLayout(self.topBox)
+ self.outerBox.addLayout(self.mainBox)
+ self.outerBox.addWidget(self.buttonBox)
+ self.outerBox.setSpacing(CONFIG.pxInt(8))
+
+ self.setLayout(self.outerBox)
+ self.setSizeGripEnabled(True)
+
+ # Connect Signals
+ self.novelSelector.novelSelectionChanged.connect(self.overviewPage.novelValueChanged)
+ self.novelSelector.novelSelectionChanged.connect(self.contentsPage.novelValueChanged)
+
+ logger.debug("Ready: GuiNovelDetails")
+
+ return
+
+ def __del__(self) -> None: # pragma: no cover
+ logger.debug("Delete: GuiNovelDetails")
+ return
+
+ ##
+ # Methods
+ ##
+
+ def updateValues(self) -> None:
+ """Load the dialogs initial values."""
+ self.overviewPage.updateProjectData()
+ self.overviewPage.novelValueChanged(self.novelSelector.handle)
+ self.contentsPage.novelValueChanged(self.novelSelector.handle)
+ return
+
+ ##
+ # Events
+ ##
+
+ def closeEvent(self, event: QCloseEvent) -> None:
+ """Capture the user closing the window and save settings."""
+ self._saveSettings()
+ event.accept()
+ self.deleteLater()
+ return
+
+ ##
+ # Private Slots
+ ##
+
+ @pyqtSlot(int)
+ def _sidebarClicked(self, pageId: int) -> None:
+ """Process a user request to switch page."""
+ if pageId == self.PAGE_OVERVIEW:
+ self.mainStack.setCurrentWidget(self.overviewPage)
+ elif pageId == self.PAGE_CONTENTS:
+ self.mainStack.setCurrentWidget(self.contentsPage)
+ return
+
+ ##
+ # Internal Functions
+ ##
+
+ def _saveSettings(self) -> None:
+ """Save the user GUI settings."""
+ winWidth = CONFIG.rpxInt(self.width())
+ winHeight = CONFIG.rpxInt(self.height())
+ novelRoot = self.novelSelector.handle
+
+ logger.debug("Saving State: GuiNovelDetails")
+ options = SHARED.project.options
+ options.setValue("GuiNovelDetails", "winWidth", winWidth)
+ options.setValue("GuiNovelDetails", "winHeight", winHeight)
+ options.setValue("GuiNovelDetails", "novelRoot", novelRoot)
+
+ self.contentsPage.saveSettings()
+
+ return
+
+# END Class GuiNovelDetails
+
+
+class _OverviewPage(NScrollablePage):
+
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
+
+ mPx = CONFIG.pxInt(8)
+ sPx = CONFIG.pxInt(16)
+ hPx = CONFIG.pxInt(24)
+ vPx = CONFIG.pxInt(4)
+
+ # Project Info
+ self.projLabel = NColourLabel(
+ self.tr("Project"), SHARED.theme.helpText,
+ parent=self, scale=NColourLabel.HEADER_SCALE
+ )
+
+ self.projName = QLabel("", self)
+ self.projWords = QLabel("", self)
+ self.projNovels = QLabel("", self)
+ self.projNotes = QLabel("", self)
+ self.projRevisions = QLabel("", self)
+ self.projEditTime = QLabel("", self)
+
+ self.projForm = QFormLayout()
+ self.projForm.addRow("{0}".format(self.tr("Name")), self.projName)
+ self.projForm.addRow("{0}".format(self.tr("Revisions")), self.projRevisions)
+ self.projForm.addRow("{0}".format(self.tr("Editing Time")), self.projEditTime)
+ self.projForm.addRow("{0}".format(self.tr("Word Count")), self.projWords)
+ self.projForm.addRow("\u2026 {0}".format(self.tr("In Novels")), self.projNovels)
+ self.projForm.addRow("\u2026 {0}".format(self.tr("In Notes ")), self.projNotes)
+ self.projForm.setContentsMargins(mPx, 0, 0, 0)
+ self.projForm.setHorizontalSpacing(hPx)
+ self.projForm.setVerticalSpacing(vPx)
+
+ # Novel Info
+ self.novelLabel = NColourLabel(
+ self.tr("Selected Novel"), SHARED.theme.helpText,
+ parent=self, scale=NColourLabel.HEADER_SCALE
+ )
+
+ self.novelName = QLabel("", self)
+ self.novelWords = QLabel("", self)
+ self.novelChapters = QLabel("", self)
+ self.novelScenes = QLabel("", self)
+
+ self.novelForm = QFormLayout()
+ self.novelForm.addRow("{0}".format(self.tr("Name")), self.novelName)
+ self.novelForm.addRow("{0}".format(self.tr("Word Count")), self.novelWords)
+ self.novelForm.addRow("{0}".format(self.tr("Chapters")), self.novelChapters)
+ self.novelForm.addRow("{0}".format(self.tr("Scenes")), self.novelScenes)
+ self.novelForm.setContentsMargins(mPx, 0, 0, 0)
+ self.novelForm.setHorizontalSpacing(hPx)
+ self.novelForm.setVerticalSpacing(vPx)
+
+ # Assemble
+ self.outerBox = QVBoxLayout()
+ self.outerBox.addWidget(self.projLabel)
+ self.outerBox.addLayout(self.projForm)
+ self.outerBox.addWidget(self.novelLabel)
+ self.outerBox.addLayout(self.novelForm)
+ self.outerBox.setSpacing(sPx)
+ self.outerBox.addStretch(1)
+
+ self.setCentralLayout(self.outerBox)
+
+ return
+
+ ##
+ # Methods
+ ##
+
+ def updateProjectData(self) -> None:
+ """Load information about the project."""
+ project = SHARED.project
+ project.updateWordCounts()
+ wcNovel, wcNotes = project.data.currCounts
+
+ self.projName.setText(project.data.name)
+ self.projRevisions.setText(f"{project.data.saveCount:n}")
+ self.projEditTime.setText(formatTime(project.currentEditTime))
+ self.projWords.setText(f"{wcNovel + wcNotes:n}")
+ self.projNovels.setText(f"{wcNovel:n}")
+ self.projNotes.setText(f"{wcNotes:n}")
+ return
+
+ ##
+ # Public Slots
+ ##
+
+ @pyqtSlot(str)
+ def novelValueChanged(self, tHandle: str) -> None:
+ """Refresh the data for the selected novel."""
+ project = SHARED.project
+ if nwItem := project.tree[tHandle]:
+ self.novelName.setText(nwItem.itemName)
+
+ nwCount = project.index.getNovelWordCount(rootHandle=tHandle)
+ self.novelWords.setText(f"{nwCount:n}")
+
+ hCounts = project.index.getNovelTitleCounts(rootHandle=tHandle)
+ self.novelChapters.setText(f"{hCounts[2]:n}")
+ self.novelScenes.setText(f"{hCounts[3]:n}")
+
+ return
+
+# END Class _OverviewPage
+
+
+class _ContentsPage(NFixedPage):
+
+ C_TITLE = 0
+ C_WORDS = 1
+ C_PAGES = 2
+ C_PAGE = 3
+ C_PROG = 4
+
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
+
+ self._data = []
+ self._currentRoot = None
+
+ iPx = SHARED.theme.baseIconSize
+ hPx = CONFIG.pxInt(12)
+ vPx = CONFIG.pxInt(4)
+ options = SHARED.project.options
+
+ # Title
+ self.contentLabel = NColourLabel(
+ self.tr("Table of Contents"), SHARED.theme.helpText,
+ parent=self, scale=NColourLabel.HEADER_SCALE
+ )
+
+ # Contents Tree
+ self.tocTree = QTreeWidget(self)
+ self.tocTree.setIconSize(QSize(iPx, iPx))
+ self.tocTree.setIndentation(0)
+ self.tocTree.setColumnCount(6)
+ self.tocTree.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
+ self.tocTree.setHeaderLabels([
+ self.tr("Title"),
+ self.tr("Words"),
+ self.tr("Pages"),
+ self.tr("Page"),
+ self.tr("Progress"),
+ "",
+ ])
+
+ treeHeadItem = self.tocTree.headerItem()
+ if treeHeadItem:
+ treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignmentFlag.AlignRight)
+ treeHeadItem.setTextAlignment(self.C_PAGES, Qt.AlignmentFlag.AlignRight)
+ treeHeadItem.setTextAlignment(self.C_PAGE, Qt.AlignmentFlag.AlignRight)
+ treeHeadItem.setTextAlignment(self.C_PROG, Qt.AlignmentFlag.AlignRight)
+
+ treeHeader = self.tocTree.header()
+ treeHeader.setStretchLastSection(True)
+ treeHeader.setMinimumSectionSize(hPx)
+
+ wCol0 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol0", 200))
+ wCol1 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol1", 60))
+ wCol2 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol2", 60))
+ wCol3 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol3", 60))
+ wCol4 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol4", 90))
+
+ self.tocTree.setColumnWidth(0, wCol0)
+ self.tocTree.setColumnWidth(1, wCol1)
+ self.tocTree.setColumnWidth(2, wCol2)
+ self.tocTree.setColumnWidth(3, wCol3)
+ self.tocTree.setColumnWidth(4, wCol4)
+ self.tocTree.setColumnWidth(5, hPx)
+
+ # Options
+ wordsPerPage = options.getInt("GuiNovelDetails", "wordsPerPage", 350)
+ countFrom = options.getInt("GuiNovelDetails", "countFrom", 1)
+ clearDouble = options.getBool("GuiNovelDetails", "clearDouble", True)
+
+ self.wpLabel = QLabel(self.tr("Words per page"))
+
+ self.wpValue = QSpinBox(self)
+ self.wpValue.setMinimum(10)
+ self.wpValue.setMaximum(1000)
+ self.wpValue.setSingleStep(10)
+ self.wpValue.setValue(wordsPerPage)
+ self.wpValue.valueChanged.connect(self._populateTree)
+
+ self.poLabel = QLabel(self.tr("First page offset"))
+
+ self.poValue = QSpinBox(self)
+ self.poValue.setMinimum(1)
+ self.poValue.setMaximum(9999)
+ self.poValue.setSingleStep(1)
+ self.poValue.setValue(countFrom)
+ self.poValue.valueChanged.connect(self._populateTree)
+
+ self.dblLabel = QLabel(self.tr("Chapters on odd pages"))
+
+ self.dblValue = NSwitch(self, 2*iPx, iPx)
+ self.dblValue.setChecked(clearDouble)
+ self.dblValue.clicked.connect(self._populateTree)
+
+ self.optionsBox = QGridLayout()
+ self.optionsBox.addWidget(self.wpLabel, 0, 0)
+ self.optionsBox.addWidget(self.wpValue, 0, 1)
+ self.optionsBox.addWidget(self.dblLabel, 0, 3)
+ self.optionsBox.addWidget(self.dblValue, 0, 4)
+ self.optionsBox.addWidget(self.poLabel, 1, 0)
+ self.optionsBox.addWidget(self.poValue, 1, 1)
+ self.optionsBox.setHorizontalSpacing(hPx)
+ self.optionsBox.setVerticalSpacing(vPx)
+ self.optionsBox.setColumnStretch(2, 1)
+
+ # Assemble
+ self.outerBox = QVBoxLayout()
+ self.outerBox.addWidget(self.contentLabel)
+ self.outerBox.addWidget(self.tocTree)
+ self.outerBox.addLayout(self.optionsBox)
+
+ self.setCentralLayout(self.outerBox)
+
+ return
+
+ def saveSettings(self) -> None:
+ """Save the user GUI settings."""
+ widthCol0 = CONFIG.rpxInt(self.tocTree.columnWidth(0))
+ widthCol1 = CONFIG.rpxInt(self.tocTree.columnWidth(1))
+ widthCol2 = CONFIG.rpxInt(self.tocTree.columnWidth(2))
+ widthCol3 = CONFIG.rpxInt(self.tocTree.columnWidth(3))
+ widthCol4 = CONFIG.rpxInt(self.tocTree.columnWidth(4))
+
+ options = SHARED.project.options
+ options.setValue("GuiNovelDetails", "widthCol0", widthCol0)
+ options.setValue("GuiNovelDetails", "widthCol1", widthCol1)
+ options.setValue("GuiNovelDetails", "widthCol2", widthCol2)
+ options.setValue("GuiNovelDetails", "widthCol3", widthCol3)
+ options.setValue("GuiNovelDetails", "widthCol4", widthCol4)
+ options.setValue("GuiNovelDetails", "wordsPerPage", self.wpValue.value())
+ options.setValue("GuiNovelDetails", "countFrom", self.poValue.value())
+ options.setValue("GuiNovelDetails", "clearDouble", self.dblValue.isChecked())
+ return
+
+ ##
+ # Public Slots
+ ##
+
+ @pyqtSlot(str)
+ def novelValueChanged(self, tHandle: str) -> None:
+ """Refresh the tree with another root item."""
+ if tHandle != self._currentRoot:
+ self._prepareData(tHandle)
+ self._populateTree()
+ self._currentRoot = tHandle
+ return
+
+ ##
+ # Private Slots
+ ##
+
+ @pyqtSlot()
+ def _populateTree(self) -> None:
+ """Set the content of the chapter/page tree."""
+ dblPages = self.dblValue.isChecked()
+ wpPage = self.wpValue.value()
+ fstPage = self.poValue.value() - 1
+
+ pTotal = 0
+ tPages = 1
+
+ theList = []
+ for _, tLevel, tTitle, wCount in self._data:
+ pCount = math.ceil(wCount/wpPage)
+ if dblPages:
+ pCount += pCount%2
+
+ pTotal += pCount
+ theList.append((tLevel, tTitle, wCount, pCount))
+
+ pMax = pTotal - fstPage
+
+ self.tocTree.clear()
+ for tLevel, tTitle, wCount, pCount in theList:
+ newItem = QTreeWidgetItem()
+
+ if tPages <= fstPage:
+ progPage = numberToRoman(tPages, True)
+ progText = ""
+ else:
+ cPage = tPages - fstPage
+ pgProg = 100.0*(cPage - 1)/pMax if pMax > 0 else 0.0
+ progPage = f"{cPage:n}"
+ progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%"
+
+ hDec = SHARED.theme.getHeaderDecoration(tLevel)
+ if tTitle.strip() == "":
+ tTitle = self.tr("Untitled")
+
+ newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
+ newItem.setText(self.C_TITLE, tTitle)
+ newItem.setText(self.C_WORDS, f"{wCount:n}")
+ newItem.setText(self.C_PAGES, f"{pCount:n}")
+ newItem.setText(self.C_PAGE, progPage)
+ newItem.setText(self.C_PROG, progText)
+
+ newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
+ newItem.setTextAlignment(self.C_PAGES, Qt.AlignRight)
+ newItem.setTextAlignment(self.C_PAGE, Qt.AlignRight)
+ newItem.setTextAlignment(self.C_PROG, Qt.AlignRight)
+
+ # Make pages and titles/partitions stand out
+ if tLevel < 2:
+ bFont = newItem.font(self.C_TITLE)
+ if tLevel == 0:
+ bFont.setItalic(True)
+ else:
+ bFont.setBold(True)
+ bFont.setUnderline(True)
+ newItem.setFont(self.C_TITLE, bFont)
+
+ tPages += pCount
+
+ self.tocTree.addTopLevelItem(newItem)
+
+ return
+
+ ##
+ # Internal Functions
+ ##
+
+ def _prepareData(self, rootHandle: str | None) -> None:
+ """Extract the information from the project index."""
+ logger.debug("Populating ToC from handle '%s'", rootHandle)
+ self._data = SHARED.project.index.getTableOfContents(rootHandle, 2)
+ self._data.append(("", 0, self.tr("END"), 0))
+ return
+
+# END Class _ContentsPage
diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py
index d136cec6..385f7bd2 100644
--- a/novelwriter/tools/welcome.py
+++ b/novelwriter/tools/welcome.py
@@ -25,7 +25,6 @@ from __future__ import annotations
import logging
-from typing import TYPE_CHECKING
from pathlib import Path
from datetime import datetime
@@ -43,14 +42,11 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED, __version__, __date__
from novelwriter.enum import nwItemClass
-from novelwriter.common import formatInt, makeFileNameSafe
+from novelwriter.common import formatInt, formatVersion, makeFileNameSafe
from novelwriter.constants import nwUnicode
from novelwriter.core.coretools import ProjectBuilder
from novelwriter.extensions.switch import NSwitch
-if TYPE_CHECKING: # pragma: no cover
- from novelwriter.guimain import GuiMain
-
logger = logging.getLogger(__name__)
@@ -58,8 +54,8 @@ class GuiWelcome(QDialog):
openProjectRequest = pyqtSignal(Path)
- def __init__(self, mainGui: GuiMain) -> None:
- super().__init__(parent=mainGui)
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
logger.debug("Create: GuiWelcome")
self.setObjectName("GuiWelcome")
@@ -91,7 +87,8 @@ class GuiWelcome(QDialog):
self.nwLabel.setPixmap(self.nwImage)
self.nwInfo = QLabel(self.tr("Version {0} {1} Released on {2}").format(
- __version__, nwUnicode.U_ENDASH, datetime.strptime(__date__, "%Y-%m-%d").strftime("%x")
+ formatVersion(__version__), nwUnicode.U_ENDASH,
+ datetime.strptime(__date__, "%Y-%m-%d").strftime("%x")
))
self.tabOpen = _OpenProjectPage(self)
@@ -343,7 +340,7 @@ class _ProjectListItem(QStyledItemDelegate):
self._dFont = qApp.font()
self._dFont.setPointSizeF(fPt)
- self._dPen = QPen(QColor(*SHARED.theme.helpText))
+ self._dPen = QPen(SHARED.theme.helpText)
self._icon = SHARED.theme.getPixmap("proj_nwx", (iPx, iPx))
diff --git a/pkgutils.py b/pkgutils.py
index 2b821e1a..fe648e8f 100755
--- a/pkgutils.py
+++ b/pkgutils.py
@@ -23,6 +23,7 @@ General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+from __future__ import annotations
import os
import sys
@@ -42,7 +43,7 @@ OS_DARWIN = 3
# Utilities
# =============================================================================================== #
-def extractVersion(beQuiet=False):
+def extractVersion(beQuiet: bool = False) -> tuple[str, str, str]:
"""Extract the novelWriter version number without having to import
anything else from the main package.
"""
@@ -73,34 +74,31 @@ def extractVersion(beQuiet=False):
return numVers, hexVers, relDate
-def compactVersion(version):
- """Make the version number more compact."""
- return version.replace("-alpha", "a").replace("-beta", "b").replace("-rc", "rc")
+def stripVersion(version: str) -> str:
+ """Strip the pre-release part from a version number."""
+ if "a" in version:
+ return version.partition("a")[0]
+ elif "b" in version:
+ return version.partition("b")[0]
+ elif "rc" in version:
+ return version.partition("rc")[0]
+ else:
+ return version
-def sysCall(callArgs, cwd=None):
- """Wrapper function for system calls."""
- sysP = subprocess.Popen(
- callArgs, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- shell=True, cwd=cwd
- )
- stdOut, stdErr = sysP.communicate()
- return stdOut.decode("utf-8"), stdErr.decode("utf-8"), sysP.returncode
-
-
-def readFile(fileName):
+def readFile(fileName: str) -> str:
"""Read an entire file and return as a string."""
with open(fileName, mode="r", encoding="utf-8") as inFile:
return inFile.read()
-def writeFile(fileName, writeText):
+def writeFile(fileName: str, writeText: str) -> None:
"""Write string to file."""
with open(fileName, mode="w+", encoding="utf-8") as outFile:
outFile.write(writeText)
-def toUpload(srcPath, dstName=None):
+def toUpload(srcPath: str, dstName: str | None = None) -> None:
"""Copy a file produced by one of the build functions to the upload
directory. The file can optionally be given a new name.
"""
@@ -113,7 +111,7 @@ def toUpload(srcPath, dstName=None):
return
-def makeCheckSum(sumFile, cwd=None):
+def makeCheckSum(sumFile: str, cwd: str | None = None) -> str:
"""Create a SHA256 checksum file."""
try:
if cwd is None:
@@ -139,7 +137,7 @@ def makeCheckSum(sumFile, cwd=None):
# Package Installer (pip)
##
-def installPackages(hostOS):
+def installPackages(hostOS: int) -> None:
"""Install package dependencies both for this script and for running
novelWriter itself.
"""
@@ -172,7 +170,7 @@ def installPackages(hostOS):
# Clean Build and Dist Folders (build-clean)
##
-def cleanBuildDirs():
+def cleanBuildDirs() -> None:
"""Recursively delete the 'build' and 'dist' folders."""
print("")
print("Cleaning up build environment ...")
@@ -208,7 +206,7 @@ def cleanBuildDirs():
# Build PDF Manual (manual)
##
-def buildPdfManual():
+def buildPdfManual() -> None:
"""This function will build the documentation as manual.pdf."""
print("")
print("Building PDF Manual")
@@ -262,7 +260,7 @@ def buildPdfManual():
# Qt Linguist QM Builder (qtlrelease)
##
-def buildQtI18n():
+def buildQtI18n() -> None:
"""Build the lang.qm files for Qt Linguist."""
print("")
print("Building Qt Localisation Files")
@@ -315,7 +313,7 @@ def buildQtI18n():
# Qt Linguist TS Builder (qtlupdate)
##
-def buildQtI18nTS(sysArgs):
+def buildQtI18nTS(sysArgs: list[str]) -> None:
"""Build the lang.ts files for Qt Linguist."""
print("")
print("Building Qt Translation Files")
@@ -392,16 +390,16 @@ def buildQtI18nTS(sysArgs):
##
-# Generage MacOS PList
+# Generate MacOS PList
##
-def genMacOSPlist():
+def genMacOSPlist() -> None:
"""Set necessary values for .plist file for MacOS build."""
outDir = "setup/macos"
- numVers = extractVersion()[0].partition("-")[0]
+ numVers = stripVersion(extractVersion()[0])
copyrightYear = datetime.datetime.now().year
- # These keys are no longer used but are present for compatability
+ # These keys are no longer used but are present for compatibility
pkgVersMaj, pkgVersMin = numVers.split(".")[:2]
plistXML = readFile(f"{outDir}/Info.plist.template").format(
@@ -422,7 +420,7 @@ def genMacOSPlist():
# Sample Project ZIP File Builder (sample)
##
-def buildSampleZip():
+def buildSampleZip() -> None:
"""Bundle the sample project into a single zip file to be saved into
the novelwriter/assets folder for further bundling into builds.
"""
@@ -459,7 +457,7 @@ def buildSampleZip():
return
-def cleanBuiltAssets():
+def cleanBuiltAssets() -> None:
"""Remove assets built by this script."""
print("")
print("Removing Built Assets")
@@ -488,7 +486,7 @@ def cleanBuiltAssets():
return
-def checkAssetsExist():
+def checkAssetsExist() -> bool:
"""Check that the necessary compiled assets exist ahead of a build.
"""
hasSample = False
@@ -523,7 +521,7 @@ def checkAssetsExist():
# Import Translations (import-i18n)
##
-def importI18nUpdates(sysArgs):
+def importI18nUpdates(sysArgs: list[str]) -> None:
"""Import new translation files from a zip file."""
print("")
print("Import Updated Translations")
@@ -563,7 +561,7 @@ def importI18nUpdates(sysArgs):
# Make Minimal Package (minimal-zip)
##
-def makeMinimalPackage(targetOS):
+def makeMinimalPackage(targetOS: int) -> None:
"""Pack the core source file in a single zip file."""
from zipfile import ZipFile, ZIP_DEFLATED
@@ -598,8 +596,7 @@ def makeMinimalPackage(targetOS):
# Build Minimal Zip
# =================
- numVers, _, _ = extractVersion()
- pkgVers = compactVersion(numVers)
+ pkgVers, _, _ = extractVersion()
zipFile = f"novelwriter-{pkgVers}-minimal{targName}.zip"
outFile = os.path.join(bldDir, zipFile)
if os.path.isfile(outFile):
@@ -679,7 +676,8 @@ def makeMinimalPackage(targetOS):
# Make Debian Package (build-deb)
##
-def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buildName=""):
+def makeDebianPackage(signKey: str | None = None, sourceBuild: bool = False,
+ distName: str = "unstable", buildName: str = "") -> str:
"""Build a Debian package."""
print("")
print("Build Debian Package")
@@ -692,13 +690,12 @@ def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buil
# ============
numVers, hexVers, relDate = extractVersion()
- pkgVers = compactVersion(numVers)
relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d")
pkgDate = email.utils.format_datetime(relDate.replace(hour=12, tzinfo=None))
print("")
- if buildName:
- pkgVers = f"{pkgVers}{buildName}"
+ pkgVers = numVers.replace("a", "~a").replace("b", "~b").replace("rc", "~rc")
+ pkgVers = f"{pkgVers}+{buildName}" if buildName else pkgVers
# Set Up Folder
# =============
@@ -839,11 +836,7 @@ def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buil
print("")
if sourceBuild:
- if hexVers[-2] == "f":
- ppaName = "novelwriter"
- else:
- ppaName = "novelwriter-pre"
-
+ ppaName = "novelwriter" if hexVers[-2] == "f" else "novelwriter-pre"
return f"dput {ppaName}/{distName} {bldDir}/{bldPkg}_source.changes"
return ""
@@ -853,14 +846,14 @@ def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buil
# Make Launchpad Package (build-ubuntu)
##
-def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False):
+def makeForLaunchpad(doSign: bool = False, isFirst: bool = False) -> None:
"""Wrapper for building Debian packages for Launchpad."""
print("")
print("Launchpad Packages")
print("==================")
print("")
- if isFirst or isSnapshot:
+ if isFirst:
bldNum = "0"
else:
bldNum = input("Build number [0]: ")
@@ -870,17 +863,12 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False):
distLoop = [
("20.04", "focal"),
("22.04", "jammy"),
- ("23.04", "lunar"),
("23.10", "mantic"),
+ ("24.04", "noble"),
]
- tStamp = datetime.datetime.now().strftime("%Y%m%d~%H%M%S")
- if isSnapshot:
- print(f"Building Ununtu SNAPSHOT~{tStamp} for:")
- print("")
- else:
- print("Building Ubuntu packages for:")
- print("")
+ print("Building Ubuntu packages for:")
+ print("")
for distNum, codeName in distLoop:
print(f" * Ubuntu {distNum} {codeName.title()}")
print("")
@@ -895,11 +883,7 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False):
dputCmd = []
for distNum, codeName in distLoop:
- if isSnapshot:
- buildName = f"+SNAPSHOT~{tStamp}~ubuntu{distNum}.0"
- else:
- buildName = f"~ubuntu{distNum}.{bldNum}"
-
+ buildName = f"ubuntu{distNum}.{bldNum}"
dCmd = makeDebianPackage(
signKey=signKey,
sourceBuild=True,
@@ -922,7 +906,7 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False):
# Make AppImage (build-appimage)
##
-def makeAppImage(sysArgs):
+def makeAppImage(sysArgs: list[str]) -> list[str]:
"""Build an AppImage."""
import glob
import argparse
@@ -968,8 +952,7 @@ def makeAppImage(sysArgs):
# Version Info
# ============
- numVers, _, relDate = extractVersion()
- pkgVers = compactVersion(numVers)
+ pkgVers, _, relDate = extractVersion()
relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d")
print("")
@@ -1139,7 +1122,7 @@ def makeAppImage(sysArgs):
# Make Windows Setup EXE (build-win-exe)
##
-def makeWindowsEmbedded(sysArgs):
+def makeWindowsEmbedded(sysArgs: list[str]) -> None:
"""Set up a package with embedded Python and dependencies for
Windows installation.
"""
@@ -1371,7 +1354,7 @@ def makeWindowsEmbedded(sysArgs):
# XDG Installation (xdg-install)
##
-def xdgInstall():
+def xdgInstall() -> None:
"""Will attempt to install icons and make a launcher."""
print("")
print("XDG Install")
@@ -1507,7 +1490,7 @@ def xdgInstall():
# XDG Uninstallation (xdg-uninstall)
##
-def xdgUninstall():
+def xdgUninstall() -> None:
"""Will attempt to uninstall icons and the launcher."""
print("")
print("XDG Uninstall")
@@ -1577,7 +1560,7 @@ def xdgUninstall():
# WIN Installation (win-install)
##
-def winInstall():
+def winInstall() -> None:
"""Will attempt to install icons and make a launcher for Windows."""
import winreg
try:
@@ -1704,7 +1687,7 @@ def winInstall():
# WIN Uninstallation (win-uninstall)
##
-def winUninstall():
+def winUninstall() -> None:
"""Will attempt to uninstall icons previously installed."""
import winreg
try:
@@ -1805,40 +1788,35 @@ if __name__ == "__main__":
else:
hostOS = OS_NONE
+ sysArgs = sys.argv.copy()
+
# Set Target OS
- if "--target-linux" in sys.argv:
- sys.argv.remove("--target-linux")
+ if "--target-linux" in sysArgs:
+ sysArgs.remove("--target-linux")
targetOS = OS_LINUX
- elif "--target-darwin" in sys.argv:
- sys.argv.remove("--target-darwin")
+ elif "--target-darwin" in sysArgs:
+ sysArgs.remove("--target-darwin")
targetOS = OS_DARWIN
- elif "--target-win" in sys.argv:
- sys.argv.remove("--target-win")
+ elif "--target-win" in sysArgs:
+ sysArgs.remove("--target-win")
targetOS = OS_WIN
else:
targetOS = hostOS
# Sign package
- if "--sign" in sys.argv:
- sys.argv.remove("--sign")
+ if "--sign" in sysArgs:
+ sysArgs.remove("--sign")
doSign = True
else:
doSign = False
# First build
- if "--first" in sys.argv:
- sys.argv.remove("--first")
+ if "--first" in sysArgs:
+ sysArgs.remove("--first")
isFirstBuild = True
else:
isFirstBuild = False
- # Build snapshot
- if "--snapshot" in sys.argv:
- sys.argv.remove("--snapshot")
- isSnapshot = True
- else:
- isSnapshot = False
-
helpMsg = [
"",
"novelWriter Setup Tool",
@@ -1856,7 +1834,7 @@ if __name__ == "__main__":
"",
" help Print the help message.",
" pip Install all package dependencies for novelWriter using pip.",
- " version Print the novelWriter version. Add -c for short version.",
+ " version Print the novelWriter version.",
" build-clean Will attempt to delete 'build' and 'dist' folders.",
"",
"Additional Builds:",
@@ -1880,7 +1858,6 @@ if __name__ == "__main__":
" sign package.",
" build-ubuntu Build a .deb packages Launchpad. Add --sign to ",
" sign package. Add --first to set build number to 0.",
- " Add --snapshot to make a snapshot package.",
" build-win-exe Build a setup.exe file with Python embedded for Windows.",
" The package must be built from a minimal windows zip file.",
" build-appimage Build an AppImage. Argument --linux-tag defaults to",
@@ -1905,71 +1882,66 @@ if __name__ == "__main__":
# General
# =======
- if "help" in sys.argv:
- sys.argv.remove("help")
+ if "help" in sysArgs:
+ sysArgs.remove("help")
print("\n".join(helpMsg))
sys.exit(0)
- if "version" in sys.argv:
- sys.argv.remove("version")
- numVers, _, _ = extractVersion(beQuiet=True)
- if "-c" in sys.argv:
- sys.argv.remove("-c")
- print(compactVersion(numVers), end=None)
- else:
- print(numVers, end=None)
+ if "version" in sysArgs:
+ sysArgs.remove("version")
+ print(extractVersion(beQuiet=True)[0], end=None)
sys.exit(0)
- if "pip" in sys.argv:
- sys.argv.remove("pip")
+ if "pip" in sysArgs:
+ sysArgs.remove("pip")
installPackages(hostOS)
- if "build-clean" in sys.argv:
- sys.argv.remove("build-clean")
+ if "build-clean" in sysArgs:
+ sysArgs.remove("build-clean")
cleanBuildDirs()
# Additional Builds
# =================
- if "manual" in sys.argv:
- sys.argv.remove("manual")
+ if "manual" in sysArgs:
+ sysArgs.remove("manual")
buildPdfManual()
- if "qtlrelease" in sys.argv:
- sys.argv.remove("qtlrelease")
+ if "qtlrelease" in sysArgs:
+ sysArgs.remove("qtlrelease")
buildQtI18n()
- if "qtlupdate" in sys.argv:
- sys.argv.remove("qtlupdate")
- buildQtI18nTS(sys.argv)
+ if "qtlupdate" in sysArgs:
+ sysArgs.remove("qtlupdate")
+ buildQtI18nTS(sysArgs)
sys.exit(0) # Don't continue execution
- if "sample" in sys.argv:
- sys.argv.remove("sample")
+ if "sample" in sysArgs:
+ sysArgs.remove("sample")
buildSampleZip()
- if "clean-assets" in sys.argv:
- sys.argv.remove("clean-assets")
+ if "clean-assets" in sysArgs:
+ sysArgs.remove("clean-assets")
cleanBuiltAssets()
- if "gen-plist" in sys.argv:
- sys.argv.remove("gen-plist")
+ if "gen-plist" in sysArgs:
+ sysArgs.remove("gen-plist")
genMacOSPlist()
# Python Packaging
# ================
- if "import-i18n" in sys.argv:
- sys.argv.remove("import-i18n")
- importI18nUpdates(sys.argv)
+ if "import-i18n" in sysArgs:
+ sysArgs.remove("import-i18n")
+ importI18nUpdates(sysArgs)
sys.exit(0) # Don't continue execution
- if "minimal-zip" in sys.argv:
- sys.argv.remove("minimal-zip")
+ if "minimal-zip" in sysArgs:
+ sysArgs.remove("minimal-zip")
makeMinimalPackage(targetOS)
- if "build-deb" in sys.argv:
- sys.argv.remove("build-deb")
+ if "build-deb" in sysArgs:
+ sysArgs.remove("build-deb")
if hostOS == OS_LINUX:
if doSign:
signKey = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08"
@@ -1980,23 +1952,23 @@ if __name__ == "__main__":
print("ERROR: Command 'build-deb' can only be used on Linux")
sys.exit(1)
- if "build-ubuntu" in sys.argv:
- sys.argv.remove("build-ubuntu")
+ if "build-ubuntu" in sysArgs:
+ sysArgs.remove("build-ubuntu")
if hostOS == OS_LINUX:
- makeForLaunchpad(doSign=doSign, isFirst=isFirstBuild, isSnapshot=isSnapshot)
+ makeForLaunchpad(doSign=doSign, isFirst=isFirstBuild)
else:
print("ERROR: Command 'build-ubuntu' can only be used on Linux")
sys.exit(1)
- if "build-win-exe" in sys.argv:
- sys.argv.remove("build-win-exe")
- makeWindowsEmbedded(sys.argv)
+ if "build-win-exe" in sysArgs:
+ sysArgs.remove("build-win-exe")
+ makeWindowsEmbedded(sysArgs)
sys.exit(0) # Don't continue execution
- if "build-appimage" in sys.argv:
- sys.argv.remove("build-appimage")
+ if "build-appimage" in sysArgs:
+ sysArgs.remove("build-appimage")
if hostOS == OS_LINUX:
- sys.argv = makeAppImage(sys.argv) # Build appimage and prune its args
+ sysArgs = makeAppImage(sysArgs)
else:
print("ERROR: Command 'build-appimage' can only be used on Linux")
sys.exit(1)
@@ -2004,32 +1976,32 @@ if __name__ == "__main__":
# General Installers
# ==================
- if "xdg-install" in sys.argv:
- sys.argv.remove("xdg-install")
+ if "xdg-install" in sysArgs:
+ sysArgs.remove("xdg-install")
if hostOS == OS_WIN:
print("ERROR: Command 'xdg-install' cannot be used on Windows")
sys.exit(1)
else:
xdgInstall()
- if "xdg-uninstall" in sys.argv:
- sys.argv.remove("xdg-uninstall")
+ if "xdg-uninstall" in sysArgs:
+ sysArgs.remove("xdg-uninstall")
if hostOS == OS_WIN:
print("ERROR: Command 'xdg-uninstall' cannot be used on Windows")
sys.exit(1)
else:
xdgUninstall()
- if "win-install" in sys.argv:
- sys.argv.remove("win-install")
+ if "win-install" in sysArgs:
+ sysArgs.remove("win-install")
if hostOS == OS_WIN:
winInstall()
else:
print("ERROR: Command 'win-install' can only be used on Windows")
sys.exit(1)
- if "win-uninstall" in sys.argv:
- sys.argv.remove("win-uninstall")
+ if "win-uninstall" in sysArgs:
+ sysArgs.remove("win-uninstall")
if hostOS == OS_WIN:
winUninstall()
else:
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index 9206abf4..8d02b22d 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,8 +1,7 @@
-
-
+
+
Sample Project
- Sample Project
Jane Smith
diff --git a/setup/macos/build.sh b/setup/macos/build.sh
index dbc9834e..1ffca83f 100755
--- a/setup/macos/build.sh
+++ b/setup/macos/build.sh
@@ -24,7 +24,7 @@ echo "Build Dir: $BUILD_DIR"
pushd "$SRC_DIR" || exit 1
-VERSION="$(python3 pkgutils.py version -c)"
+VERSION="$(python3 pkgutils.py version)"
echo "novelWriter Version: $VERSION"
# --- Prepare Files ----------------------------------------------------------------------------- #
diff --git a/setup/make_snapshot.sh b/setup/make_snapshot.sh
deleted file mode 100755
index 5b92350c..00000000
--- a/setup/make_snapshot.sh
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/bin/bash
-set -e
-
-ENVPATH=/tmp/nwBuild
-
-if [ ! -f pkgutils.py ]; then
- echo "Must be called from the root folder of the source"
- exit 1
-fi
-
-echo ""
-echo " Building Dependencies"
-echo "================================================================================"
-echo ""
-if [ ! -d $ENVPATH ]; then
- python3 -m venv $ENVPATH
-fi
-source $ENVPATH/bin/activate
-pip3 install -r docs/source/requirements.txt
-python3 pkgutils.py clean-assets
-python3 pkgutils.py qtlrelease manual sample
-deactivate
-
-echo ""
-echo " Building Linux Snapshots"
-echo "================================================================================"
-echo ""
-python3 pkgutils.py build-ubuntu --sign --snapshot
diff --git a/tests/files/nwProject-1.5.nwx b/tests/files/nwProject-1.5.nwx
index 1965de73..cc17f909 100644
--- a/tests/files/nwProject-1.5.nwx
+++ b/tests/files/nwProject-1.5.nwx
@@ -1,8 +1,7 @@
-
+
Sample Project
- Sample Project
Jane Smith
diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
index 6fd70fb2..99b5a57c 100644
--- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx
+++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
@@ -1,8 +1,7 @@
-
+
New Project
- New Novel
Jane Doe
diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx
index 057e3742..028da14c 100644
--- a/tests/reference/coreProject_NewRoot_nwProject.nwx
+++ b/tests/reference/coreProject_NewRoot_nwProject.nwx
@@ -1,8 +1,7 @@
-
+
New Project
- New Novel
Jane Doe
diff --git a/tests/reference/coreTools_DocDuplicator_nwProject.nwx b/tests/reference/coreTools_DocDuplicator_nwProject.nwx
index 0ee99397..bab9a82d 100644
--- a/tests/reference/coreTools_DocDuplicator_nwProject.nwx
+++ b/tests/reference/coreTools_DocDuplicator_nwProject.nwx
@@ -1,8 +1,7 @@
-
+
New Project
- New Novel
Jane Doe
diff --git a/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx b/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx
index 02f11a29..ee5eeeea 100644
--- a/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx
+++ b/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx
@@ -1,8 +1,7 @@
-
+
Test Project A
- Test Project A
Jane Doe
diff --git a/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx b/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx
index 0df054a8..209c7809 100644
--- a/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx
+++ b/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx
@@ -1,8 +1,7 @@
-
+
Test Project B
- Test Project B
Jane Doe
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx
index c56d9782..c3ee941f 100644
--- a/tests/reference/guiEditor_Main_Final_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx
@@ -1,8 +1,7 @@
-
-
+
+
New Project
- New Novel
Jane Doe
diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
index e266f65f..aa9c3a74 100644
--- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
@@ -1,8 +1,7 @@
-
+
New Project
- New Novel
Jane Doe
diff --git a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json
index 429aff11..7c315e5a 100644
--- a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json
+++ b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json
@@ -1,10 +1,9 @@
{
"meta": {
"projectName": "Lorem Ipsum",
- "novelTitle": "Lorem Ipsum",
"novelAuthor": "lipsum.com",
- "buildTime": 1687620769,
- "buildTimeStr": "2023-06-24 17:32:49"
+ "buildTime": 1706306020,
+ "buildTimeStr": "2024-01-26 22:53:40"
},
"text": {
"css": [
diff --git a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json
index 305b0006..06633aa2 100644
--- a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json
+++ b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json
@@ -1,10 +1,9 @@
{
"meta": {
"projectName": "Lorem Ipsum",
- "novelTitle": "Lorem Ipsum",
"novelAuthor": "lipsum.com",
- "buildTime": 1687621222,
- "buildTimeStr": "2023-06-24 17:40:22"
+ "buildTime": 1706306020,
+ "buildTimeStr": "2024-01-26 22:53:40"
},
"text": {
"nwd": [
diff --git a/tests/reference/projectXML_ReadLegacy10.nwx b/tests/reference/projectXML_ReadLegacy10.nwx
index 9d04ef44..5161eb5b 100644
--- a/tests/reference/projectXML_ReadLegacy10.nwx
+++ b/tests/reference/projectXML_ReadLegacy10.nwx
@@ -1,8 +1,7 @@
-
+
Sample Project
- Sample Project
Jay Doh
diff --git a/tests/reference/projectXML_ReadLegacy11.nwx b/tests/reference/projectXML_ReadLegacy11.nwx
index 50924c78..7624a057 100644
--- a/tests/reference/projectXML_ReadLegacy11.nwx
+++ b/tests/reference/projectXML_ReadLegacy11.nwx
@@ -1,8 +1,7 @@
-
+
Sample Project
- Sample Project
Jay Doh
diff --git a/tests/reference/projectXML_ReadLegacy12.nwx b/tests/reference/projectXML_ReadLegacy12.nwx
index 7d36e380..58b29061 100644
--- a/tests/reference/projectXML_ReadLegacy12.nwx
+++ b/tests/reference/projectXML_ReadLegacy12.nwx
@@ -1,8 +1,7 @@
-
+
Sample Project
- Sample Project
Jay Doh
diff --git a/tests/reference/projectXML_ReadLegacy13.nwx b/tests/reference/projectXML_ReadLegacy13.nwx
index c75aa97c..28e24c69 100644
--- a/tests/reference/projectXML_ReadLegacy13.nwx
+++ b/tests/reference/projectXML_ReadLegacy13.nwx
@@ -1,8 +1,7 @@
-
+
Sample Project
- Sample Project
Jay Doh
diff --git a/tests/reference/projectXML_ReadLegacy14.nwx b/tests/reference/projectXML_ReadLegacy14.nwx
index aef603dd..26e7413c 100644
--- a/tests/reference/projectXML_ReadLegacy14.nwx
+++ b/tests/reference/projectXML_ReadLegacy14.nwx
@@ -1,8 +1,7 @@
-
+
Sample Project
- Sample Project
Jay Doh
diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py
index 0a475c19..a45571cf 100644
--- a/tests/test_base/test_base_common.py
+++ b/tests/test_base/test_base_common.py
@@ -35,7 +35,7 @@ from PyQt5.QtCore import QUrl
from novelwriter.common import (
checkBool, checkFloat, checkHandle, checkInt, checkIntTuple, checkPath,
checkString, checkStringNone, checkUuid, formatInt, formatTime,
- formatTimeStamp, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass,
+ formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass,
isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax,
numberToRoman, NWConfigParser, openExternalPath, readTextFile, simplified,
transferCase, xmlIndent, yesNo
@@ -348,6 +348,17 @@ def testBaseCommon_formatTime():
# END Test testBaseCommon_formatTime
+@pytest.mark.base
+def testBaseCommon_formatVersion():
+ """Test the formatVersion function."""
+ assert formatVersion("1.2") == "1.2"
+ assert formatVersion("1.2a1") == "1.2 Alpha 1"
+ assert formatVersion("1.2b2") == "1.2 Beta 2"
+ assert formatVersion("1.2rc3") == "1.2 RC 3"
+
+# END Test testBaseCommon_formatVersion
+
+
@pytest.mark.base
def testBaseCommon_simplified():
"""Test the simplified function."""
diff --git a/tests/test_core/test_core_docbuild.py b/tests/test_core/test_core_docbuild.py
index 8890e2a8..b70269a8 100644
--- a/tests/test_core/test_core_docbuild.py
+++ b/tests/test_core/test_core_docbuild.py
@@ -230,7 +230,7 @@ def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
assert error == []
copyfile(docFile, tstFile)
- assert cmpFiles(tstFile, cmpFile, ignoreLines=[6, 7])
+ assert cmpFiles(tstFile, cmpFile, ignoreLines=[5, 6])
# Check Error Handling
# ====================
@@ -370,7 +370,7 @@ def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
assert error == []
copyfile(docFile, tstFile)
- assert cmpFiles(tstFile, cmpFile, ignoreLines=[6, 7])
+ assert cmpFiles(tstFile, cmpFile, ignoreLines=[5, 6])
# Check Error Handling
# ====================
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py
index 5e9dee3f..44836349 100644
--- a/tests/test_core/test_core_index.py
+++ b/tests/test_core/test_core_index.py
@@ -695,6 +695,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
"This is a story about Jane Smith.\n\n"
"Well, not really. She's still awesome though.\n"
))
+
# Whole document
cC, wC, pC = index.getCounts(nHandle)
assert cC == 152
@@ -798,6 +799,10 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
# Extract stats
assert index.getNovelWordCount(activeOnly=False) == 43
assert index.getNovelWordCount(activeOnly=True) == 15
+ assert index.getNovelWordCount(rootHandle=C.hNovelRoot, activeOnly=False) == 43
+ assert index.getNovelWordCount(rootHandle=C.hNovelRoot, activeOnly=True) == 15
+ assert index.getNovelWordCount(rootHandle=C.hWorldRoot, activeOnly=False) == 0
+ assert index.getNovelWordCount(rootHandle=C.hWorldRoot, activeOnly=True) == 0
assert index.getNovelTitleCounts(activeOnly=False) == [0, 3, 2, 3, 0]
assert index.getNovelTitleCounts(activeOnly=True) == [0, 1, 2, 3, 0]
diff --git a/tests/test_core/test_core_options.py b/tests/test_core/test_core_options.py
index 7458c746..fd291d08 100644
--- a/tests/test_core/test_core_options.py
+++ b/tests/test_core/test_core_options.py
@@ -121,27 +121,27 @@ def testCoreOptions_SetGet(mockGUI):
assert options.setValue("GuiProjectSettings", "winWidth", 100) is True
# Set some values of different types
- assert options.setValue("GuiProjectDetails", "winWidth", 100) is True
- assert options.setValue("GuiProjectDetails", "winHeight", 12.34) is True
- assert options.setValue("GuiProjectDetails", "clearDouble", True) is True
+ assert options.setValue("GuiNovelDetails", "winWidth", 100) is True
+ assert options.setValue("GuiNovelDetails", "winHeight", 12.34) is True
+ assert options.setValue("GuiNovelDetails", "clearDouble", True) is True
assert options.setValue("GuiNovelView", "lastCol", nwColHidden) is True
# Generic get, doesn't check type
- assert options.getValue("GuiProjectDetails", "winWidth", None) == 100
- assert options.getValue("GuiProjectDetails", "winHeight", None) == 12.34
- assert options.getValue("GuiProjectDetails", "clearDouble", None) is True
- assert options.getValue("GuiProjectDetails", "mockItem", None) is None
+ assert options.getValue("GuiNovelDetails", "winWidth", None) == 100
+ assert options.getValue("GuiNovelDetails", "winHeight", None) == 12.34
+ assert options.getValue("GuiNovelDetails", "clearDouble", None) is True
+ assert options.getValue("GuiNovelDetails", "mockItem", None) is None
# Get type-specific
- assert options.getString("GuiProjectDetails", "winWidth", None) is None # type: ignore
- assert options.getString("GuiProjectDetails", "mockItem", None) is None # type: ignore
- assert options.getInt("GuiProjectDetails", "winWidth", None) == 100 # type: ignore
- assert options.getInt("GuiProjectDetails", "textFont", None) is None # type: ignore
- assert options.getInt("GuiProjectDetails", "mockItem", None) is None # type: ignore
- assert options.getFloat("GuiProjectDetails", "winWidth", None) == 100.0 # type: ignore
- assert options.getFloat("GuiProjectDetails", "mockItem", None) is None # type: ignore
- assert options.getBool("GuiProjectDetails", "clearDouble", None) is True # type: ignore
- assert options.getBool("GuiProjectDetails", "mockItem", None) is None # type: ignore
+ assert options.getString("GuiNovelDetails", "winWidth", None) is None # type: ignore
+ assert options.getString("GuiNovelDetails", "mockItem", None) is None # type: ignore
+ assert options.getInt("GuiNovelDetails", "winWidth", None) == 100 # type: ignore
+ assert options.getInt("GuiNovelDetails", "textFont", None) is None # type: ignore
+ assert options.getInt("GuiNovelDetails", "mockItem", None) is None # type: ignore
+ assert options.getFloat("GuiNovelDetails", "winWidth", None) == 100.0 # type: ignore
+ assert options.getFloat("GuiNovelDetails", "mockItem", None) is None # type: ignore
+ assert options.getBool("GuiNovelDetails", "clearDouble", None) is True # type: ignore
+ assert options.getBool("GuiNovelDetails", "mockItem", None) is None # type: ignore
assert options.getEnum("GuiNovelView", "lastCol", NovelTreeColumn, nwColHidden) == nwColHidden
# Get from non-existent groups
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index 538e179b..fdad6b77 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -482,10 +482,6 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
project.data.setName(" A Name ")
assert project.data.name == "A Name"
- # Project Title
- project.data.setTitle(" A Title ")
- assert project.data.title == "A Title"
-
# Project Author
project.data.setAuthor(" Jane\tDoe ")
assert project.data.author == "Jane Doe"
diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py
index 7753722b..554da1ef 100644
--- a/tests/test_core/test_core_projectxml.py
+++ b/tests/test_core/test_core_projectxml.py
@@ -131,13 +131,12 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
assert xmlReader.state == XMLReadState.PARSED_OK
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0105
- assert xmlReader.xmlRevision == 1
+ assert xmlReader.xmlRevision == 2
assert xmlReader.appVersion == "2.0-rc1"
assert xmlReader.hexVersion == 0x020000c1
# Check loaded data
assert data.name == "Sample Project"
- assert data.title == "Sample Project"
assert data.author == "Jane Smith"
assert data.saveCount == 5
assert data.autoCount == 10
@@ -256,7 +255,6 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockRnd):
# Check loaded data
assert data.name == "Sample Project"
- assert data.title == "Sample Project"
assert data.author == "Jay Doh" # Only last author is preserved
assert data.saveCount == 0 # Doesn't exist in 1.0
assert data.autoCount == 0 # Doesn't exist in 1.0
@@ -391,7 +389,6 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockRnd):
# Check loaded data
assert data.name == "Sample Project"
- assert data.title == "Sample Project"
assert data.author == "Jay Doh" # Only last author is preserved
assert data.saveCount == 5
assert data.autoCount == 10
@@ -526,7 +523,6 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockRnd):
# Check loaded data
assert data.name == "Sample Project"
- assert data.title == "Sample Project"
assert data.author == "Jay Doh" # Only last author is preserved
assert data.saveCount == 5
assert data.autoCount == 10
@@ -664,7 +660,6 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd):
# Check loaded data
assert data.name == "Sample Project"
- assert data.title == "Sample Project"
assert data.author == "Jay Doh" # Only last author is preserved
assert data.saveCount == 5
assert data.autoCount == 10
@@ -802,7 +797,6 @@ def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockRnd):
# Check loaded data
assert data.name == "Sample Project"
- assert data.title == "Sample Project"
assert data.author == "Jay Doh" # Only last author is preserved
assert data.saveCount == 5
assert data.autoCount == 10
diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py
index c4c2e590..408865c7 100644
--- a/tests/test_dialogs/test_dlg_preferences.py
+++ b/tests/test_dialogs/test_dlg_preferences.py
@@ -22,9 +22,11 @@ from __future__ import annotations
import pytest
+from tools import getGuiItem
+
from PyQt5.QtGui import QFontDatabase, QKeyEvent
from PyQt5.QtCore import QEvent, Qt
-from PyQt5.QtWidgets import QDialogButtonBox, QFileDialog, QFontDialog
+from PyQt5.QtWidgets import QAction, QDialogButtonBox, QFileDialog, QFontDialog
from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwConst, nwUnicode
@@ -38,9 +40,13 @@ KEY_DELAY = 1
def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths):
"""Test the preferences dialog loading."""
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")])
+ monkeypatch.setattr(GuiPreferences, "exec_", lambda *a: None)
# Load GUI with standard values
- prefs = GuiPreferences(nwGUI)
+ nwGUI.mainMenu.aPreferences.activate(QAction.ActionEvent.Trigger)
+ qtbot.waitUntil(lambda: getGuiItem("GuiPreferences") is not None, timeout=1000)
+ prefs = getGuiItem("GuiPreferences")
+ assert isinstance(prefs, GuiPreferences)
prefs.show()
# Check Languages
@@ -95,10 +101,6 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
# Check Navigation
vBar = prefs.mainForm.verticalScrollBar()
old = -1
- with qtbot.waitSignal(vBar.valueChanged) as value:
- prefs.sidebar.button(0).click()
- assert value.args[0] > old
- old = value.args[0]
with qtbot.waitSignal(vBar.valueChanged) as value:
prefs.sidebar.button(1).click()
assert value.args[0] > old
@@ -107,6 +109,10 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
prefs.sidebar.button(2).click()
assert value.args[0] > old
old = value.args[0]
+ with qtbot.waitSignal(vBar.valueChanged) as value:
+ prefs.sidebar.button(3).click()
+ assert value.args[0] > old
+ old = value.args[0]
# Check Search
prefs.searchText.setText("Display language")
diff --git a/tests/test_dialogs/test_dlg_projdetails.py b/tests/test_dialogs/test_dlg_projdetails.py
deleted file mode 100644
index 3d68f9d7..00000000
--- a/tests/test_dialogs/test_dlg_projdetails.py
+++ /dev/null
@@ -1,109 +0,0 @@
-"""
-novelWriter – Project Details Dialog Class Tester
-=================================================
-
-This file is a part of novelWriter
-Copyright 2018–2024, Veronica Berglyd Olsen
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful, but
-WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-"""
-from __future__ import annotations
-
-import pytest
-
-from tools import getGuiItem
-
-from PyQt5.QtWidgets import QAction
-
-from novelwriter import SHARED
-from novelwriter.dialogs.projdetails import GuiProjectDetails
-
-
-@pytest.mark.gui
-def testDlgProjDetails_Dialog(qtbot, nwGUI, prjLipsum):
- """Test the project details dialog.
- """
- # Create a project to work on
- assert nwGUI.openProject(prjLipsum)
- assert nwGUI.rebuildIndex(beQuiet=True)
- qtbot.wait(100)
-
- # Open the Writing Stats dialog
- nwGUI.mainMenu.aProjectDetails.activate(QAction.Trigger)
- qtbot.waitUntil(lambda: getGuiItem("GuiProjectDetails") is not None, timeout=1000)
-
- projDet = getGuiItem("GuiProjectDetails")
- assert isinstance(projDet, GuiProjectDetails)
-
- # Overview Page
- # =============
-
- assert projDet.tabMain.bookTitle.text() == "Lorem Ipsum"
- assert projDet.tabMain.projName.text()[-11:] == "Lorem Ipsum"
- assert projDet.tabMain.bookAuthors.text()[-10:] == "lipsum.com"
-
- assert projDet.tabMain.wordCountVal.text() == f"{3000:n}"
- assert projDet.tabMain.chapCountVal.text() == f"{3:n}"
- assert projDet.tabMain.sceneCountVal.text() == f"{5:n}"
- assert projDet.tabMain.revCountVal.text() == f"{SHARED.project.data.saveCount:n}"
-
- assert projDet.tabMain.projPathVal.text() == str(prjLipsum)
-
- # Contents Page
- # =============
-
- tocTab = projDet.tabContents
- tocTree = tocTab.tocTree
- assert tocTree.topLevelItemCount() == 7
- assert tocTree.topLevelItem(0).text(tocTab.C_TITLE) == "Lorem Ipsum" # type: ignore
- assert tocTree.topLevelItem(2).text(tocTab.C_TITLE) == "Prologue" # type: ignore
- assert tocTree.topLevelItem(3).text(tocTab.C_TITLE) == "Act One" # type: ignore
- assert tocTree.topLevelItem(4).text(tocTab.C_TITLE) == "Chapter One" # type: ignore
- assert tocTree.topLevelItem(5).text(tocTab.C_TITLE) == "Chapter Two" # type: ignore
- assert tocTree.topLevelItem(6).text(tocTab.C_TITLE) == "END" # type: ignore
-
- # Count Pages
- tocTab.wpValue.setValue(100)
- tocTab.poValue.setValue(4)
- tocTab.dblValue.setChecked(False)
- tocTab._populateTree()
-
- thePages = ["1", "2", "1", "1", "11", "17", "0"]
- thePage = ["i", "ii", "1", "2", "3", "14", "31"]
- for i in range(7):
- assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == thePages[i] # type: ignore
- assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == thePage[i] # type: ignore
-
- tocTab.poValue.setValue(5)
- tocTab.dblValue.setChecked(True)
- tocTab._populateTree()
-
- thePages = ["2", "2", "2", "2", "12", "18", "0"]
- thePage = ["i", "iii", "1", "3", "5", "17", "35"]
- for i in range(7):
- assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == thePages[i] # type: ignore
- assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == thePage[i] # type: ignore
-
- # Re-populate
- assert tocTab._currentRoot is None
- tocTab._novelValueChanged("7a992350f3eb6") # Not a root
- assert tocTab._currentRoot == "b3643d0f92e32" # The actual novel root
-
- # qtbot.stop()
-
- # Clean Up
- projDet.close()
- nwGUI.closeMain()
-
-# END Test testDlgProjDetails_Dialog
diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projectsettings.py
similarity index 50%
rename from tests/test_dialogs/test_dlg_projsettings.py
rename to tests/test_dialogs/test_dlg_projectsettings.py
index 37556245..5b4d5499 100644
--- a/tests/test_dialogs/test_dlg_projsettings.py
+++ b/tests/test_dialogs/test_dlg_projectsettings.py
@@ -31,7 +31,7 @@ from PyQt5.QtWidgets import QDialog, QAction, QColorDialog
from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwItemType
from novelwriter.dialogs.editlabel import GuiEditLabel
-from novelwriter.dialogs.projsettings import GuiProjectSettings
+from novelwriter.dialogs.projectsettings import GuiProjectSettings
KEY_DELAY = 1
@@ -63,17 +63,17 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI):
qtbot.addWidget(projSettings)
# Switch Tabs
- projSettings._focusTab(GuiProjectSettings.TAB_REPLACE)
- assert projSettings._tabBox.currentWidget() == projSettings.tabReplace
+ projSettings.sidebar.button(GuiProjectSettings.PAGE_SETTINGS).click()
+ assert projSettings.mainStack.currentWidget() == projSettings.settingsPage
- projSettings._focusTab(GuiProjectSettings.TAB_IMPORT)
- assert projSettings._tabBox.currentWidget() == projSettings.tabImport
+ projSettings.sidebar.button(GuiProjectSettings.PAGE_STATUS).click()
+ assert projSettings.mainStack.currentWidget() == projSettings.statusPage
- projSettings._focusTab(GuiProjectSettings.TAB_STATUS)
- assert projSettings._tabBox.currentWidget() == projSettings.tabStatus
+ projSettings.sidebar.button(GuiProjectSettings.PAGE_IMPORT).click()
+ assert projSettings.mainStack.currentWidget() == projSettings.importPage
- projSettings._focusTab(GuiProjectSettings.TAB_MAIN)
- assert projSettings._tabBox.currentWidget() == projSettings.tabMain
+ projSettings.sidebar.button(GuiProjectSettings.PAGE_REPLACE).click()
+ assert projSettings.mainStack.currentWidget() == projSettings.replacePage
# Clean Up
projSettings.close()
@@ -83,9 +83,11 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI):
@pytest.mark.gui
-def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
- """Test the main tab of the project settings dialog."""
- monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")])
+def testDlgProjSettings_SettingsPage(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
+ """Test the settings page of the dialog."""
+ languages = [("en", "English"), ("de", "German")]
+ monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda *a: languages)
+ monkeypatch.setattr(CONFIG, "listLanguages", lambda *a: languages)
# Create new project
buildTestProject(nwGUI, projPath)
@@ -94,72 +96,57 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockR
# Set some values
project = SHARED.project
+ project.data.setLanguage("en")
project.data.setSpellLang("en")
project.data.setAuthor("Jane Smith")
project.data.setAutoReplace({"A": "B", "C": "D"})
# Create Dialog
- projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_MAIN)
+ projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_SETTINGS)
projSettings.show()
qtbot.addWidget(projSettings)
# Settings Tab
- # ============
+ settings = projSettings.settingsPage
- tabMain = projSettings.tabMain
+ assert settings.projName.text() == "New Project"
+ assert settings.projAuthor.text() == "Jane Smith"
+ assert settings.projLang.currentData() == "en"
+ assert settings.spellLang.currentData() == "en"
+ assert settings.doBackup.isChecked() is False
- assert tabMain.editName.text() == "New Project"
- assert tabMain.editTitle.text() == "New Novel"
- assert tabMain.editAuthor.text() == "Jane Smith"
- assert tabMain.spellLang.currentData() == "en"
- assert tabMain.doBackup.isChecked() is False
-
- tabMain.editName.setText("")
- for c in "Project Name":
- qtbot.keyClick(tabMain.editName, c, delay=KEY_DELAY)
- tabMain.editTitle.setText("")
- for c in "Project Title":
- qtbot.keyClick(tabMain.editTitle, c, delay=KEY_DELAY)
-
- tabMain.editAuthor.clear()
- for c in "Jane Doe":
- qtbot.keyClick(tabMain.editAuthor, c, delay=KEY_DELAY)
-
- assert tabMain.editName.text() == "Project Name"
- assert tabMain.editTitle.text() == "Project Title"
- assert tabMain.editAuthor.text() == "Jane Doe"
+ settings.projName.setText("Project Name")
+ settings.projAuthor.setText("Jane Doe")
+ settings.projLang.setCurrentIndex(settings.projLang.findData("de"))
+ settings.spellLang.setCurrentIndex(settings.spellLang.findData("de"))
+ settings.doBackup.setChecked(True)
projSettings._doSave()
assert project.data.name == "Project Name"
- assert project.data.title == "Project Title"
assert project.data.author == "Jane Doe"
+ assert project.data.language == "de"
+ assert project.data.spellLang == "de"
+ assert project.data.doBackup is False
- nwGUI._processProjectSettingsChanges()
+ nwGUI._processProjectSettingsChanges(False)
assert nwGUI.windowTitle() == "novelWriter - Project Name"
# qtbot.stop()
-# END Test testDlgProjSettings_Main
+# END Test testDlgProjSettings_SettingsPage
@pytest.mark.gui
-def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
- """Test the status and importance tabs of the project settings
- dialog.
- """
+def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
+ """Test the status and importance pages of the dialog."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
- monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")])
-
- # Create new project
- mockRnd.reset()
buildTestProject(nwGUI, projPath)
- CONFIG.setBackupPath(fncPath)
# Set some values
- theProject = SHARED.project
- theProject.tree[C.hTitlePage].setStatus(C.sFinished) # type: ignore
- theProject.tree[C.hChapterDoc].setStatus(C.sDraft) # type: ignore
- theProject.tree[C.hSceneDoc].setStatus(C.sDraft) # type: ignore
+ project = SHARED.project
+ project.tree[C.hTitlePage].setStatus(C.sFinished) # type: ignore
+ project.tree[C.hChapterDoc].setStatus(C.sDraft) # type: ignore
+ project.tree[C.hSceneDoc].setStatus(C.sDraft) # type: ignore
nwGUI.projView.projTree.setSelectedHandle(C.hPlotRoot)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
@@ -172,51 +159,54 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat
hCharNote = "0000000000011"
hWorldNote = "0000000000012"
- theProject.tree[hPlotNote].setImport(C.iMajor) # type: ignore
- theProject.tree[hCharNote].setImport(C.iMajor) # type: ignore
- theProject.tree[hWorldNote].setImport(C.iMain) # type: ignore
+ project.tree[hPlotNote].setImport(C.iMajor) # type: ignore
+ project.tree[hCharNote].setImport(C.iMajor) # type: ignore
+ project.tree[hWorldNote].setImport(C.iMain) # type: ignore
+
+ nwGUI.rebuildTrees()
+ project.countStatus()
+
+ assert [e["count"] for _, e in project.data.itemStatus.items()] == [2, 0, 2, 1]
+ assert [e["count"] for _, e in project.data.itemImport.items()] == [3, 0, 2, 1]
# Create Dialog
- projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_STATUS)
+ projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_STATUS)
projSettings.show()
qtbot.addWidget(projSettings)
# Status Tab
# ==========
- tabStatus = projSettings.tabStatus
+ status = projSettings.statusPage
- assert tabStatus.colChanged is False
- assert tabStatus.getNewList() == ([], [])
- assert tabStatus.listBox.topLevelItemCount() == 4
+ assert status.wasChanged is False
+ assert status.getNewList() == ([], [])
+ assert status.listBox.topLevelItemCount() == 4
# Can't delete the first item (it's in use)
- tabStatus.listBox.clearSelection()
- tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(0))
- qtbot.mouseClick(tabStatus.delButton, Qt.LeftButton)
- assert tabStatus.listBox.topLevelItemCount() == 4
+ status.listBox.clearSelection()
+ status.listBox.setCurrentItem(status.listBox.topLevelItem(0))
+ qtbot.mouseClick(status.delButton, Qt.LeftButton)
+ assert status.listBox.topLevelItemCount() == 4
# Can delete the second item
- tabStatus.listBox.clearSelection()
- tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(1))
- qtbot.mouseClick(tabStatus.delButton, Qt.LeftButton)
- assert tabStatus.listBox.topLevelItemCount() == 3
+ status.listBox.clearSelection()
+ status.listBox.setCurrentItem(status.listBox.topLevelItem(1))
+ qtbot.mouseClick(status.delButton, Qt.LeftButton)
+ assert status.listBox.topLevelItemCount() == 3
# Add a new item
with monkeypatch.context() as mp:
mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40))
- qtbot.mouseClick(tabStatus.addButton, Qt.LeftButton)
- tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(3))
- for _ in range(8):
- qtbot.keyClick(tabStatus.editName, Qt.Key_Backspace, delay=KEY_DELAY)
- for c in "Final":
- qtbot.keyClick(tabStatus.editName, c, delay=KEY_DELAY)
- qtbot.mouseClick(tabStatus.colButton, Qt.LeftButton)
- qtbot.mouseClick(tabStatus.saveButton, Qt.LeftButton)
- assert tabStatus.listBox.topLevelItemCount() == 4
+ status.addButton.click()
+ status.listBox.setCurrentItem(status.listBox.topLevelItem(3))
+ status.editName.setText("Final")
+ status.colButton.click()
+ status.saveButton.click()
+ assert status.listBox.topLevelItemCount() == 4
- assert tabStatus.colChanged is True
- assert tabStatus.getNewList() == (
+ assert status.wasChanged is True
+ assert status.getNewList() == (
[
{
"key": C.sNew,
@@ -241,62 +231,62 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat
)
# Move items, none selected -> no change
- tabStatus.listBox.clearSelection()
- tabStatus._moveItem(1)
- assert [x["key"] for x in tabStatus.getNewList()[0]] == [
+ status.listBox.clearSelection()
+ status._moveItem(1)
+ assert [x["key"] for x in status.getNewList()[0]] == [
C.sNew, C.sDraft, C.sFinished, None
]
# Move items, first selected, move up -> no change
- tabStatus.listBox.clearSelection()
- tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(0))
- tabStatus._moveItem(-1)
- assert [x["key"] for x in tabStatus.getNewList()[0]] == [
+ status.listBox.clearSelection()
+ status.listBox.setCurrentItem(status.listBox.topLevelItem(0))
+ status._moveItem(-1)
+ assert [x["key"] for x in status.getNewList()[0]] == [
C.sNew, C.sDraft, C.sFinished, None
]
# Move items, last selected, move up -> allowed
- tabStatus.listBox.clearSelection()
- tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(3))
- tabStatus._moveItem(-1)
- assert [x["key"] for x in tabStatus.getNewList()[0]] == [
+ status.listBox.clearSelection()
+ status.listBox.setCurrentItem(status.listBox.topLevelItem(3))
+ status._moveItem(-1)
+ assert [x["key"] for x in status.getNewList()[0]] == [
C.sNew, C.sDraft, None, C.sFinished
]
# Move items, same selected, move down -> allowed
- tabStatus._moveItem(1)
- assert [x["key"] for x in tabStatus.getNewList()[0]] == [
+ status._moveItem(1)
+ assert [x["key"] for x in status.getNewList()[0]] == [
C.sNew, C.sDraft, C.sFinished, None
]
# Importance Tab
# ==============
- tabImport = projSettings.tabImport
- projSettings._focusTab(GuiProjectSettings.TAB_IMPORT)
+ importance = projSettings.importPage
+ projSettings._sidebarClicked(GuiProjectSettings.PAGE_IMPORT)
# Delete unused entry
- tabImport.listBox.clearSelection()
- tabImport.listBox.setCurrentItem(tabImport.listBox.topLevelItem(1))
- qtbot.mouseClick(tabImport.delButton, Qt.LeftButton)
- assert tabImport.listBox.topLevelItemCount() == 3
+ importance.listBox.clearSelection()
+ importance.listBox.setCurrentItem(importance.listBox.topLevelItem(1))
+ qtbot.mouseClick(importance.delButton, Qt.LeftButton)
+ assert importance.listBox.topLevelItemCount() == 3
# Add a new entry
with monkeypatch.context() as mp:
mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40))
- qtbot.mouseClick(tabImport.addButton, Qt.LeftButton)
- tabImport.listBox.clearSelection()
- tabImport.listBox.setCurrentItem(tabImport.listBox.topLevelItem(3))
+ qtbot.mouseClick(importance.addButton, Qt.LeftButton)
+ importance.listBox.clearSelection()
+ importance.listBox.setCurrentItem(importance.listBox.topLevelItem(3))
for _ in range(8):
- qtbot.keyClick(tabImport.editName, Qt.Key_Backspace, delay=KEY_DELAY)
+ qtbot.keyClick(importance.editName, Qt.Key_Backspace, delay=KEY_DELAY)
for c in "Final":
- qtbot.keyClick(tabImport.editName, c, delay=KEY_DELAY)
- qtbot.mouseClick(tabImport.colButton, Qt.LeftButton)
- qtbot.mouseClick(tabImport.saveButton, Qt.LeftButton)
- assert tabImport.listBox.topLevelItemCount() == 4
+ qtbot.keyClick(importance.editName, c, delay=KEY_DELAY)
+ qtbot.mouseClick(importance.colButton, Qt.LeftButton)
+ qtbot.mouseClick(importance.saveButton, Qt.LeftButton)
+ assert importance.listBox.topLevelItemCount() == 4
- assert tabImport.colChanged is True
- assert tabImport.getNewList() == (
+ assert importance.wasChanged is True
+ assert importance.getNewList() == (
[
{
"key": C.iNew,
@@ -323,13 +313,13 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat
# Check Project
projSettings._doSave()
- statusItems = dict(theProject.data.itemStatus.items())
+ statusItems = dict(project.data.itemStatus.items())
assert statusItems[C.sNew]["name"] == "New"
assert statusItems[C.sDraft]["name"] == "Draft"
assert statusItems[C.sFinished]["name"] == "Finished"
assert statusItems["s000013"]["name"] == "Final"
- importItems = dict(theProject.data.itemImport.items())
+ importItems = dict(project.data.itemImport.items())
assert importItems[C.iNew]["name"] == "New"
assert importItems[C.iMajor]["name"] == "Major"
assert importItems[C.iMain]["name"] == "Main"
@@ -341,83 +331,78 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat
@pytest.mark.gui
-def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
- """Test the auto-replace tab of the project settings dialog."""
+def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
+ """Test the auto-replace page of the dialog."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
- monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")])
-
- # Create new project
- mockRnd.reset()
buildTestProject(nwGUI, projPath)
- CONFIG.setBackupPath(fncPath)
# Set some values
- theProject = SHARED.project
- theProject.data.setAutoReplace({
+ project = SHARED.project
+ project.data.setAutoReplace({
"A": "B", "C": "D"
})
# Create Dialog
- projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_REPLACE)
+ projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_REPLACE)
projSettings.show()
qtbot.addWidget(projSettings)
# Auto-Replace Tab
# ================
- tabReplace = projSettings.tabReplace
+ replace = projSettings.replacePage
- assert tabReplace.listBox.topLevelItem(0).text(0) == "" # type: ignore
- assert tabReplace.listBox.topLevelItem(0).text(1) == "B" # type: ignore
- assert tabReplace.listBox.topLevelItem(1).text(0) == "" # type: ignore
- assert tabReplace.listBox.topLevelItem(1).text(1) == "D" # type: ignore
- assert tabReplace.listBox.topLevelItemCount() == 2
+ assert replace.listBox.topLevelItem(0).text(0) == "" # type: ignore
+ assert replace.listBox.topLevelItem(0).text(1) == "B" # type: ignore
+ assert replace.listBox.topLevelItem(1).text(0) == "" # type: ignore
+ assert replace.listBox.topLevelItem(1).text(1) == "D" # type: ignore
+ assert replace.listBox.topLevelItemCount() == 2
# Nothing to save or delete
- tabReplace.listBox.clearSelection()
- tabReplace._saveEntry()
- tabReplace._delEntry()
- assert tabReplace.listBox.topLevelItemCount() == 2
+ replace.listBox.clearSelection()
+ replace._saveEntry()
+ replace._delEntry()
+ assert replace.listBox.topLevelItemCount() == 2
# Create a new entry
- qtbot.mouseClick(tabReplace.addButton, Qt.LeftButton)
- assert tabReplace.listBox.topLevelItemCount() == 3
- assert tabReplace.listBox.topLevelItem(2).text(0) == "" # type: ignore
- assert tabReplace.listBox.topLevelItem(2).text(1) == "" # type: ignore
+ qtbot.mouseClick(replace.addButton, Qt.LeftButton)
+ assert replace.listBox.topLevelItemCount() == 3
+ assert replace.listBox.topLevelItem(2).text(0) == "" # type: ignore
+ assert replace.listBox.topLevelItem(2).text(1) == "" # type: ignore
# Edit the entry
- tabReplace.listBox.setCurrentItem(tabReplace.listBox.topLevelItem(2))
- tabReplace.editKey.setText("")
+ replace.listBox.setCurrentItem(replace.listBox.topLevelItem(2))
+ replace.editKey.setText("")
for c in "Th is ":
- qtbot.keyClick(tabReplace.editKey, c, delay=KEY_DELAY)
- tabReplace.editValue.setText("")
+ qtbot.keyClick(replace.editKey, c, delay=KEY_DELAY)
+ replace.editValue.setText("")
for c in "With This Stuff ":
- qtbot.keyClick(tabReplace.editValue, c, delay=KEY_DELAY)
- qtbot.mouseClick(tabReplace.saveButton, Qt.LeftButton)
- assert tabReplace.listBox.topLevelItem(2).text(0) == "" # type: ignore
- assert tabReplace.listBox.topLevelItem(2).text(1) == "With This Stuff " # type: ignore
+ qtbot.keyClick(replace.editValue, c, delay=KEY_DELAY)
+ qtbot.mouseClick(replace.saveButton, Qt.LeftButton)
+ assert replace.listBox.topLevelItem(2).text(0) == "" # type: ignore
+ assert replace.listBox.topLevelItem(2).text(1) == "With This Stuff " # type: ignore
# Create a new entry again
- tabReplace.listBox.clearSelection()
- qtbot.mouseClick(tabReplace.addButton, Qt.LeftButton)
- assert tabReplace.listBox.topLevelItemCount() == 4
+ replace.listBox.clearSelection()
+ qtbot.mouseClick(replace.addButton, Qt.LeftButton)
+ assert replace.listBox.topLevelItemCount() == 4
# The list is sorted, so we must find it
newIdx = -1
- for i in range(tabReplace.listBox.topLevelItemCount()):
- if tabReplace.listBox.topLevelItem(i).text(0) == "": # type: ignore
+ for i in range(replace.listBox.topLevelItemCount()):
+ if replace.listBox.topLevelItem(i).text(0) == "": # type: ignore
newIdx = i
break
assert newIdx >= 0
# Then delete the new item
- tabReplace.listBox.setCurrentItem(tabReplace.listBox.topLevelItem(newIdx))
- qtbot.mouseClick(tabReplace.delButton, Qt.LeftButton)
- assert tabReplace.listBox.topLevelItemCount() == 3
+ replace.listBox.setCurrentItem(replace.listBox.topLevelItem(newIdx))
+ qtbot.mouseClick(replace.delButton, Qt.LeftButton)
+ assert replace.listBox.topLevelItemCount() == 3
# Check Project
projSettings._doSave()
- assert theProject.data.autoReplace == {
+ assert project.data.autoReplace == {
"A": "B", "C": "D", "This": "With This Stuff"
}
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index 38cb7ef2..5c15ed4a 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -29,7 +29,7 @@ from tools import (
C, NWD_IGNORE, cmpFiles, buildTestProject, XML_IGNORE, getGuiItem
)
-from PyQt5.QtGui import QColor, QPalette
+from PyQt5.QtGui import QPalette
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMenu, QInputDialog
@@ -170,7 +170,7 @@ def testGuiMain_UpdateTheme(qtbot, nwGUI):
mainTheme.loadSyntax()
nwGUI._processConfigChanges(True, True, True, True)
- syntaxBack = QColor(*SHARED.theme.colBack)
+ syntaxBack = SHARED.theme.colBack
assert nwGUI.docEditor.palette().color(QPalette.ColorRole.Window) == syntaxBack
assert nwGUI.docEditor.docHeader.palette().color(QPalette.ColorRole.Window) == syntaxBack
@@ -200,7 +200,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
assert len(SHARED.project.tree._roots) == 0
assert SHARED.project.tree.trashRoot is None
assert SHARED.project.data.name == ""
- assert SHARED.project.data.title == ""
assert SHARED.project.data.author == ""
assert SHARED.project.data.spellCheck is False
@@ -220,7 +219,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
assert len(SHARED.project.tree._roots) == 4
assert SHARED.project.tree.trashRoot is None
assert SHARED.project.data.name == "New Project"
- assert SHARED.project.data.title == "New Novel"
assert SHARED.project.data.author == "Jane Doe"
assert SHARED.project.data.spellCheck is False
diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py
index 08defddc..b7a77dcd 100644
--- a/tests/test_gui/test_gui_theme.py
+++ b/tests/test_gui/test_gui_theme.py
@@ -27,7 +27,7 @@ from pathlib import Path
from mocked import causeOSError
from tools import writeFile
-from PyQt5.QtGui import QIcon, QPalette, QPixmap
+from PyQt5.QtGui import QColor, QIcon, QPalette, QPixmap
from PyQt5.QtWidgets import QApplication
from novelwriter import CONFIG, SHARED
@@ -89,29 +89,38 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths):
parser = NWConfigParser()
parser["Palette"] = {
- "colour1": "100, 150, 200",
- "colour2": "100, 150, 200, 250",
- "colour3": "250, 250",
- "colour4": "-10, 127, 300",
+ "colour1": "100, 150, 200", # Valid
+ "colour2": "100, 150, 200, 250", # With alpha
+ "colour3": "100, 150, 200, 250, 300", # Too many values
+ "colour4": "250, 250", # Missing blue
+ "colour5": "-10, 127, 300", # Invalid red and blue
+ "colour6": "bob, 127, 255", # Invalid red
}
# Test the parser for several valid and invalid values
- assert mainTheme._parseColour(parser, "Palette", "colour1") == [100, 150, 200]
- assert mainTheme._parseColour(parser, "Palette", "colour2") == [100, 150, 200]
- assert mainTheme._parseColour(parser, "Palette", "colour3") == [0, 0, 0]
- assert mainTheme._parseColour(parser, "Palette", "colour4") == [0, 127, 255]
- assert mainTheme._parseColour(parser, "Palette", "colour5") == [0, 0, 0]
+ assert mainTheme._parseColour(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255)
+ assert mainTheme._parseColour(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250)
+ assert mainTheme._parseColour(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250)
+ assert mainTheme._parseColour(parser, "Palette", "colour4").getRgb() == (250, 250, 0, 255)
+ assert mainTheme._parseColour(parser, "Palette", "colour5").getRgb() == (0, 0, 0, 0)
+ assert mainTheme._parseColour(parser, "Palette", "colour6").getRgb() == (0, 127, 255, 255)
# The palette should load with the parsed values
mainTheme._setPalette(parser, "Palette", "colour1", QPalette.Window)
assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 255)
mainTheme._setPalette(parser, "Palette", "colour2", QPalette.Window)
- assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 255)
+ assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 250)
mainTheme._setPalette(parser, "Palette", "colour3", QPalette.Window)
- assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 0, 0, 255)
+ assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 250)
mainTheme._setPalette(parser, "Palette", "colour4", QPalette.Window)
- assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 127, 255, 255)
+ assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (250, 250, 0, 255)
mainTheme._setPalette(parser, "Palette", "colour5", QPalette.Window)
+ assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 0, 0, 0)
+ mainTheme._setPalette(parser, "Palette", "colour6", QPalette.Window)
+ assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 127, 255, 255)
+
+ # Non-existing value should return default colour
+ mainTheme._setPalette(parser, "Palette", "stuff", QPalette.Window)
assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 0, 0, 255)
# qtbot.stop()
@@ -240,9 +249,9 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI):
# Check some values
assert mainTheme.syntaxName == "Default Light"
- assert mainTheme.colBack == [255, 255, 255]
- assert mainTheme.colText == [0, 0, 0]
- assert mainTheme.colLink == [0, 0, 200]
+ assert mainTheme.colBack == QColor(255, 255, 255)
+ assert mainTheme.colText == QColor(0, 0, 0)
+ assert mainTheme.colLink == QColor(0, 0, 200)
# Load Default Dark Theme
# =======================
@@ -253,9 +262,9 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI):
# Check some values
assert mainTheme.syntaxName == "Default Dark"
- assert mainTheme.colBack == [54, 54, 54]
- assert mainTheme.colText == [199, 207, 208]
- assert mainTheme.colLink == [184, 200, 0]
+ assert mainTheme.colBack == QColor(54, 54, 54)
+ assert mainTheme.colText == QColor(199, 207, 208)
+ assert mainTheme.colLink == QColor(184, 200, 0)
# qtbot.stop()
diff --git a/tests/test_tools/test_tools_noveldetails.py b/tests/test_tools/test_tools_noveldetails.py
new file mode 100644
index 00000000..c8925e53
--- /dev/null
+++ b/tests/test_tools/test_tools_noveldetails.py
@@ -0,0 +1,136 @@
+"""
+novelWriter – Novel Details Tool Tester
+=======================================
+
+This file is a part of novelWriter
+Copyright 2018–2024, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+from __future__ import annotations
+
+import pytest
+
+from tools import getGuiItem
+
+from PyQt5.QtWidgets import QAction
+
+from novelwriter import SHARED
+from novelwriter.enum import nwItemClass
+from novelwriter.tools.noveldetails import GuiNovelDetails
+
+
+@pytest.mark.gui
+def testToolNovelDetails_Main(qtbot, nwGUI, prjLipsum, ipsumText):
+ """Test the Novel Details main dialog."""
+ nwGUI.openProject(prjLipsum)
+ nHandle = "b3643d0f92e32"
+
+ # Add a second Novel folder
+ project = SHARED.project
+ secondText = "#! Second\n\n" + "\n\n".join(ipsumText)
+ sHandle = project.newRoot(nwItemClass.NOVEL, "Second")
+ dHandle = project.newFile("Document", sHandle)
+ project.storage.getDocument(dHandle).writeDocument(secondText)
+ project.index.reIndexHandle(dHandle)
+ nwGUI.projView.projTree.revealNewTreeItem(sHandle)
+ nwGUI.projView.projTree.revealNewTreeItem(dHandle)
+
+ # Create the dialog
+ nwGUI.mainMenu.aNovelDetails.activate(QAction.ActionEvent.Trigger)
+ qtbot.waitUntil(lambda: getGuiItem("GuiNovelDetails") is not None, timeout=1000)
+ details = getGuiItem("GuiNovelDetails")
+ assert isinstance(details, GuiNovelDetails)
+
+ # Overview Page
+ # =============
+ overview = details.overviewPage
+
+ # The selector should default to the first entry
+ assert details.novelSelector.handle == nHandle
+
+ # Check project data
+ assert overview.projName.text() == "Lorem Ipsum"
+ assert overview.projWords.text() == f"{4376:n}"
+ assert overview.projNovels.text() == f"{3638:n}"
+ assert overview.projNotes.text() == f"{738:n}"
+ assert overview.projRevisions.text() != ""
+ assert overview.projEditTime.text() != ""
+
+ # Check novel data for "Novel"
+ assert overview.novelName.text() == "Novel"
+ assert overview.novelWords.text() == f"{3000:n}"
+ assert overview.novelChapters.text() == f"{3:n}"
+ assert overview.novelScenes.text() == f"{5:n}"
+
+ # Check novel data for "Second"
+ details.novelSelector.setHandle(sHandle, blockSignal=False)
+ assert overview.novelName.text() == "Second"
+ assert overview.novelWords.text() == f"{529:n}"
+ assert overview.novelChapters.text() == f"{0:n}"
+ assert overview.novelScenes.text() == f"{0:n}"
+
+ # Contents Page
+ # =============
+ details.novelSelector.setHandle(nHandle, blockSignal=False)
+ details.sidebar.button(details.PAGE_CONTENTS).click()
+ assert details.mainStack.currentIndex() == 1
+ contents = details.contentsPage
+
+ # Check defaults
+ words = [f"{v:n}" for v in [40, 176, 92, 6, 1071, 1615, 0]]
+ pages = [f"{v:n}" for v in [2, 2, 2, 2, 4, 6, 0]]
+ page = [f"{v:n}" for v in [1, 3, 5, 7, 9, 13, 19]]
+ for i in range(6):
+ item = contents.tocTree.topLevelItem(i)
+ assert item is not None
+ assert item.text(contents.C_WORDS) == words[i]
+ assert item.text(contents.C_PAGES) == pages[i]
+ assert item.text(contents.C_PAGE) == page[i]
+
+ # Change Settings
+ contents.poValue.setValue(7)
+ contents.wpValue.setValue(50)
+ words = [f"{v:n}" for v in [40, 176, 92, 6, 1071, 1615, 0]]
+ pages = [f"{v:n}" for v in [2, 4, 2, 2, 22, 34, 0]]
+ page = ["i", "iii"] + [f"{v:n}" for v in [1, 3, 5, 27, 61]]
+ for i in range(6):
+ item = contents.tocTree.topLevelItem(i)
+ assert item is not None
+ assert item.text(contents.C_WORDS) == words[i]
+ assert item.text(contents.C_PAGES) == pages[i]
+ assert item.text(contents.C_PAGE) == page[i]
+
+ # Turn off use odd pages
+ contents.dblValue.setChecked(False)
+ contents.poValue.setValue(0)
+ contents.wpValue.setValue(100)
+ words = [f"{v:n}" for v in [40, 176, 92, 6, 1071, 1615, 0]]
+ pages = [f"{v:n}" for v in [1, 2, 1, 1, 11, 17, 0]]
+ page = [f"{v:n}" for v in [1, 2, 4, 5, 6, 17, 34]]
+ for i in range(6):
+ item = contents.tocTree.topLevelItem(i)
+ assert item is not None
+ assert item.text(contents.C_WORDS) == words[i]
+ assert item.text(contents.C_PAGES) == pages[i]
+ assert item.text(contents.C_PAGE) == page[i]
+
+ # Revert to Overview page
+ details.sidebar.button(details.PAGE_OVERVIEW).click()
+ assert details.mainStack.currentIndex() == 0
+
+ # qtbot.stop()
+ details.close()
+
+# END Test testToolNovelDetails_Main
diff --git a/tests/tools.py b/tests/tools.py
index 6bc21966..2da23818 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -156,52 +156,54 @@ def cleanProject(path: str | Path):
return
-def buildTestProject(obj, projPath):
+def buildTestProject(obj: object, projPath: Path) -> None:
"""Build a standard test project in projPath using the project
object as the parent.
"""
from novelwriter.enum import nwItemClass
+ from novelwriter.guimain import GuiMain
from novelwriter.core.project import NWProject
if isinstance(obj, NWProject):
nwGUI = None
project = obj
- else:
+ elif isinstance(obj, GuiMain):
from novelwriter import SHARED
nwGUI = obj
project = SHARED.project
+ else:
+ return
project.storage.createNewProject(projPath)
project.setDefaultStatusImport()
project.data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")
project.data.setName("New Project")
- project.data.setTitle("New Novel")
project.data.setAuthor("Jane Doe")
# Creating a minimal project with a few root folders and a
# single chapter folder with a single file.
- xHandle = {}
- xHandle[1] = project.newRoot(nwItemClass.NOVEL, "Novel")
- xHandle[2] = project.newRoot(nwItemClass.PLOT, "Plot")
- xHandle[3] = project.newRoot(nwItemClass.CHARACTER, "Characters")
- xHandle[4] = project.newRoot(nwItemClass.WORLD, "World")
- xHandle[5] = project.newFile("Title Page", xHandle[1])
- xHandle[6] = project.newFolder("New Chapter", xHandle[1])
- xHandle[7] = project.newFile("New Chapter", xHandle[6])
- xHandle[8] = project.newFile("New Scene", xHandle[6])
+ nrHandle = project.newRoot(nwItemClass.NOVEL, "Novel")
+ project.newRoot(nwItemClass.PLOT, "Plot")
+ project.newRoot(nwItemClass.CHARACTER, "Characters")
+ project.newRoot(nwItemClass.WORLD, "World")
- aDoc = project.storage.getDocument(xHandle[5])
+ tdHandle = project.newFile("Title Page", nrHandle)
+ cfHandle = project.newFolder("New Chapter", nrHandle) or ""
+ cdHandle = project.newFile("New Chapter", cfHandle)
+ sdHandle = project.newFile("New Scene", cfHandle)
+
+ aDoc = project.storage.getDocument(tdHandle)
aDoc.writeDocument("#! New Novel\n\n>> By Jane Doe <<\n")
- project.index.reIndexHandle(xHandle[5])
+ project.index.reIndexHandle(tdHandle)
- aDoc = project.storage.getDocument(xHandle[7])
+ aDoc = project.storage.getDocument(cdHandle)
aDoc.writeDocument("## %s\n\n" % project.tr("New Chapter"))
- project.index.reIndexHandle(xHandle[7])
+ project.index.reIndexHandle(cdHandle)
- aDoc = project.storage.getDocument(xHandle[8])
+ aDoc = project.storage.getDocument(sdHandle)
aDoc.writeDocument("### %s\n\n" % project.tr("New Scene"))
- project.index.reIndexHandle(xHandle[8])
+ project.index.reIndexHandle(sdHandle)
project.session.startSession()
project.setProjectChanged(True)