Merge branch 'main' into update_docs

This commit is contained in:
Veronica Berglyd Olsen
2024-03-09 17:39:05 +01:00
committed by GitHub
43 changed files with 11098 additions and 11229 deletions
+28
View File
@@ -1,5 +1,33 @@
# novelWriter Changelog # novelWriter Changelog
## Version 2.3 RC 1 [2024-02-24]
### Release Notes
This is a release candidate of the next release version, and is intended for testing purposes.
Please be careful when using this version on live writing projects, and make sure you take frequent
backups.
Please check the changelog for an overview of changes. The full release notes will be added to the
final release.
### Detailed Changelog
**Improvements**
* Redesign the buttons on the new Welcome dialog so that they only show buttons related to the
visible page. Drop the additional buttons on the New Project page. Issue #1706.
PRs #1707 and #1709.
* Drop the Language setting on the Welcome dialog's New Project page. PR #1707.
* Hide the additional settings for Fresh Projects on the New Project page of the new Welcome
dialog. Issue #1705. PR #1707.
* Update the Templates root folder icon. PR #1709.
* Scene separators are now hidden after a new title in manuscript builds, also when there are no
chapters. Issue #1704. PR #1711.
* Scene and chapter counters are now reset when a novel title is encountered. PR #1711.
----
## Version 2.3 Beta 1 [2024-02-16] ## Version 2.3 Beta 1 [2024-02-16]
### Release Notes ### Release Notes
+1558 -1690
View File
File diff suppressed because it is too large Load Diff
+1658 -1781
View File
File diff suppressed because it is too large Load Diff
+1658 -1781
View File
File diff suppressed because it is too large Load Diff
+1657 -1780
View File
File diff suppressed because it is too large Load Diff
+1657 -1780
View File
File diff suppressed because it is too large Load Diff
+2198 -2139
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -42,9 +42,9 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen" __author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net" __email__ = "code@vkbo.net"
__version__ = "2.3b1" __version__ = "2.3rc1"
__hexversion__ = "0x020300b1" __hexversion__ = "0x020300c1"
__date__ = "2024-02-16" __date__ = "2024-02-24"
__status__ = "Stable" __status__ = "Stable"
__domain__ = "novelwriter.io" __domain__ = "novelwriter.io"
@@ -1,7 +1,18 @@
{ {
"Synopsis": "Synopsis", "Synopsis": "Synopsis",
"Short Description": "Korte beschrijving",
"Comment": "Opmerking", "Comment": "Opmerking",
"Notes": "Notities", "Notes": "Notities",
"Tag": "Label",
"Point of View": "Perspectief",
"Focus": "Focus",
"Characters": "Personages",
"Plot": "Plot",
"Timeline": "Tijdslijn",
"Locations": "Locaties",
"Objects": "Objecten",
"Entities": "Entiteiten",
"Custom": "Aangepast",
"0": "nul", "0": "nul",
"1": "één", "1": "één",
"2": "twee", "2": "twee",
@@ -1,7 +1,18 @@
{ {
"Synopsis": "Sinopse", "Synopsis": "Sinopse",
"Short Description": "Descrição breve",
"Comment": "Comentários", "Comment": "Comentários",
"Notes": "Notas", "Notes": "Notas",
"Tag": "Etiqueta",
"Point of View": "Ponto de vista",
"Focus": "Foco",
"Characters": "Personagens",
"Plot": "Trama",
"Timeline": "Linha do tempo",
"Locations": "Lugares",
"Objects": "Objetos",
"Entities": "Entidades",
"Custom": "Outros",
"0": "Zero", "0": "Zero",
"1": "Um", "1": "Um",
"2": "Dois", "2": "Dois",
@@ -40,7 +40,7 @@ cls_none = typ_cancel.svg
cls_novel = typ_book.svg cls_novel = typ_book.svg
cls_object = typ_key.svg cls_object = typ_key.svg
cls_plot = typ_puzzle.svg cls_plot = typ_puzzle.svg
cls_template = typ_document-add-col.svg cls_template = mixed_document-new.svg
cls_timeline = typ_calendar.svg cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg cls_trash = typ_trash.svg
cls_world = typ_location.svg cls_world = typ_location.svg
@@ -60,11 +60,13 @@ fmt_superscript = nw_tb-superscript.svg
fmt_underline = nw_tb-underline.svg fmt_underline = nw_tb-underline.svg
forward = typ_chevron-right.svg forward = typ_chevron-right.svg
import = mixed_import.svg import = mixed_import.svg
list = typ_th-list.svg
maximise = typ_arrow-maximise.svg maximise = typ_arrow-maximise.svg
menu = typ_th-dot-menu.svg menu = typ_th-dot-menu.svg
minimise = typ_arrow-minimise.svg minimise = typ_arrow-minimise.svg
more = typ_th-dot-more.svg more = typ_th-dot-more.svg
noncheckable = mixed_input-none.svg noncheckable = mixed_input-none.svg
open = typ_folder.svg
panel = nw_panel.svg panel = nw_panel.svg
proj_chapter = mixed_document-chapter.svg proj_chapter = mixed_document-chapter.svg
proj_details = typ_th-list-grey.svg proj_details = typ_th-list-grey.svg
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m5.8898 3.4449c-0.67467 1e-7 -1.2225 0.5478-1.2225 1.2225v14.665c0 0.67466 0.5478 1.2225 1.2225 1.2225h12.22c0.67466 0 1.2225-0.5478 1.2225-1.2225v-11h-1.8326c-1.6854 0-3.0551-1.3696-3.0551-3.0551v-1.8326zm9.7775 0.50488v1.3277c0 1.0108 0.82184 1.8326 1.8326 1.8326h1.3277z" fill="#e5e5e5" fill-opacity=".5" stroke-linecap="round" stroke-width=".55"/>
<path d="m5.8898 1c-2.0216 0-3.6674 1.6458-3.6674 3.6674v14.665c0 2.0216 1.6458 3.6674 3.6674 3.6674h12.22c2.0216 0 3.6674-1.6458 3.6674-3.6674v-12.222c0-0.32511-0.12901-0.63512-0.35879-0.86367l-4.8877-4.8877c-0.22856-0.22978-0.53856-0.35879-0.86367-0.35879zm0 2.4449h8.5551v1.8326c0 1.6854 1.3696 3.0551 3.0551 3.0551h1.8326v11c0 0.67466-0.5478 1.2225-1.2225 1.2225h-12.22c-0.67467 0-1.2225-0.5478-1.2225-1.2225v-14.665c0-0.67467 0.5478-1.2225 1.2225-1.2225zm9.7775 0.50488 3.1604 3.1604h-1.3277c-1.0108 0-1.8326-0.82184-1.8326-1.8326z" fill="#aeaeae" stroke-width="1.2222"/>
<path d="m10.105 9.9584-3.785 0.4196c-0.13651 0.01653-0.25722 0.10777-0.30249 0.24644-0.045266 0.13867 0 0.28308 0.10059 0.37434 1.1252 1.0267 2.8158 2.5643 2.8158 2.5643-0.00143 0-0.46415 2.2367-0.77166 3.729-0.025865 0.13508 0.022992 0.27806 0.14083 0.36356 0.11711 0.0855 0.268 0.08694 0.38655 0.02012 1.3249-0.753 3.3094-1.8853 3.3094-1.8853l3.308 1.886c0.12071 0.0661 0.27159 0.06467 0.38871-0.02084 0.11783-0.0855 0.16669-0.22848 0.1401-0.36284l-0.7695-3.7297 2.8158-2.5622c0.10059-0.09412 0.14514-0.23854 0.10059-0.37649-0.04454-0.13794-0.16525-0.22919-0.30177-0.245-1.5146-0.16956-3.7857-0.42104-3.7857-0.42104l-1.5685-3.4703c-0.05892-0.1243-0.1825-0.21124-0.32764-0.21124-0.14514 0-0.26944 0.087654-0.32548 0.21124z" fill="#ffff84" stroke-width=".7185"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="m5.8898 3.4449c-0.67467 1e-7 -1.2225 0.5478-1.2225 1.2225v14.665c0 0.67466 0.5478 1.2225 1.2225 1.2225h12.22c0.67466 0 1.2225-0.5478 1.2225-1.2225v-11h-1.8326c-1.6854 0-3.0551-1.3696-3.0551-3.0551v-1.8326zm9.7775 0.50488v1.3277c0 1.0108 0.82184 1.8326 1.8326 1.8326h1.3277z" fill="#e5e5e5" stroke-linecap="round" stroke-width=".55"/>
<path d="m5.8898 1c-2.0216 0-3.6674 1.6458-3.6674 3.6674v14.665c0 2.0216 1.6458 3.6674 3.6674 3.6674h12.22c2.0216 0 3.6674-1.6458 3.6674-3.6674v-12.222c0-0.32511-0.12901-0.63512-0.35879-0.86367l-4.8877-4.8877c-0.22856-0.22978-0.53856-0.35879-0.86367-0.35879zm0 2.4449h8.5551v1.8326c0 1.6854 1.3696 3.0551 3.0551 3.0551h1.8326v11c0 0.67466-0.5478 1.2225-1.2225 1.2225h-12.22c-0.67467 0-1.2225-0.5478-1.2225-1.2225v-14.665c0-0.67467 0.5478-1.2225 1.2225-1.2225zm9.7775 0.50488 3.1604 3.1604h-1.3277c-1.0108 0-1.8326-0.82184-1.8326-1.8326z" fill="#aeaeae" stroke-width="1.2222"/>
<path d="m12 6c-0.82934 0-1.5 0.67081-1.5 1.5003v3.0007h-3c-0.82934 0-1.5 0.67081-1.5 1.5003 0 0.82952 0.67066 1.5003 1.5 1.5003h3v3.0007c0 0.82952 0.67066 1.4977 1.5 1.4977 0.82934 0 1.5-0.66817 1.5-1.4977v-3.0007h3c0.82934 0 1.5-0.67081 1.5-1.5003 1e-6 -0.82952-0.67066-1.5003-1.5-1.5003h-3v-3.0007c0-0.82952-0.67066-1.5003-1.5-1.5003z" fill="#69c" stroke-width="1.4999"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="#69c" stroke-width="1.0526">
<path d="m19.632 17.263h-7.3684c-1.1611 0-2.1053 0.94421-2.1053 2.1053s0.94421 2.1053 2.1053 2.1053h7.3684c1.1611 0 2.1053-0.94421 2.1053-2.1053s-0.94421-2.1053-2.1053-2.1053zm0-7.3684h-7.3684c-1.1611 0-2.1053 0.94421-2.1053 2.1053s0.94421 2.1053 2.1053 2.1053h7.3684c1.1611 0 2.1053-0.94421 2.1053-2.1053s-0.94421-2.1053-2.1053-2.1053zm0-7.3684h-7.3684c-1.1611 0-2.1053 0.94421-2.1053 2.1053 0 1.1611 0.94421 2.1053 2.1053 2.1053h7.3684c1.1611 0 2.1053-0.94421 2.1053-2.1053 0-1.1611-0.94421-2.1053-2.1053-2.1053z"/>
<circle cx="4.8947" cy="19.368" r="2.6316"/>
<circle cx="4.8947" cy="12" r="2.6316"/>
<circle cx="4.8947" cy="4.6316" r="2.6316"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 846 B

@@ -40,7 +40,7 @@ cls_none = typ_cancel.svg
cls_novel = typ_book.svg cls_novel = typ_book.svg
cls_object = typ_key.svg cls_object = typ_key.svg
cls_plot = typ_puzzle.svg cls_plot = typ_puzzle.svg
cls_template = typ_document-add-col.svg cls_template = mixed_document-new.svg
cls_timeline = typ_calendar.svg cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg cls_trash = typ_trash.svg
cls_world = typ_location.svg cls_world = typ_location.svg
@@ -60,11 +60,13 @@ fmt_superscript = nw_tb-superscript.svg
fmt_underline = nw_tb-underline.svg fmt_underline = nw_tb-underline.svg
forward = typ_chevron-right.svg forward = typ_chevron-right.svg
import = mixed_import.svg import = mixed_import.svg
list = typ_th-list.svg
maximise = typ_arrow-maximise.svg maximise = typ_arrow-maximise.svg
menu = typ_th-dot-menu.svg menu = typ_th-dot-menu.svg
minimise = typ_arrow-minimise.svg minimise = typ_arrow-minimise.svg
more = typ_th-dot-more.svg more = typ_th-dot-more.svg
noncheckable = mixed_input-none.svg noncheckable = mixed_input-none.svg
open = typ_folder.svg
panel = nw_panel.svg panel = nw_panel.svg
proj_chapter = mixed_document-chapter.svg proj_chapter = mixed_document-chapter.svg
proj_details = typ_th-list-grey.svg proj_details = typ_th-list-grey.svg
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m5.8898 3.4449c-0.67467 1e-7 -1.2225 0.5478-1.2225 1.2225v14.665c0 0.67466 0.5478 1.2225 1.2225 1.2225h12.22c0.67466 0 1.2225-0.5478 1.2225-1.2225v-11h-1.8326c-1.6854 0-3.0551-1.3696-3.0551-3.0551v-1.8326zm9.7775 0.50488v1.3277c0 1.0108 0.82184 1.8326 1.8326 1.8326h1.3277z" fill="#e5e5e5" stroke-linecap="round" stroke-width=".55"/>
<path d="m5.8898 1c-2.0216 0-3.6674 1.6458-3.6674 3.6674v14.665c0 2.0216 1.6458 3.6674 3.6674 3.6674h12.22c2.0216 0 3.6674-1.6458 3.6674-3.6674v-12.222c0-0.32511-0.12901-0.63512-0.35879-0.86367l-4.8877-4.8877c-0.22856-0.22978-0.53856-0.35879-0.86367-0.35879zm0 2.4449h8.5551v1.8326c0 1.6854 1.3696 3.0551 3.0551 3.0551h1.8326v11c0 0.67466-0.5478 1.2225-1.2225 1.2225h-12.22c-0.67467 0-1.2225-0.5478-1.2225-1.2225v-14.665c0-0.67467 0.5478-1.2225 1.2225-1.2225zm9.7775 0.50488 3.1604 3.1604h-1.3277c-1.0108 0-1.8326-0.82184-1.8326-1.8326z" fill="#333" stroke-width="1.2222"/>
<path d="m10.105 9.9584-3.785 0.4196c-0.13651 0.01653-0.25722 0.10777-0.30249 0.24644-0.04526 0.13867 0 0.28308 0.10059 0.37434 1.1252 1.0267 2.8158 2.5643 2.8158 2.5643-0.0014 0-0.46415 2.2367-0.77166 3.729-0.02587 0.13508 0.02299 0.27806 0.14082 0.36356 0.11711 0.0855 0.268 0.08694 0.38655 0.02012 1.3249-0.753 3.3094-1.8853 3.3094-1.8853l3.308 1.886c0.12071 0.0661 0.27159 0.06467 0.38871-0.02084 0.11783-0.0855 0.16669-0.22848 0.1401-0.36284l-0.7695-3.7297 2.8158-2.5622c0.10059-0.09412 0.14514-0.23854 0.10059-0.37649-0.04454-0.13795-0.16525-0.2292-0.30177-0.24501-1.5146-0.16956-3.7857-0.42103-3.7857-0.42103l-1.5685-3.4703c-0.05892-0.1243-0.1825-0.21124-0.32764-0.21124-0.14514 0-0.26944 0.087654-0.32548 0.21124z" fill="#eab700" stroke-width=".7185"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="m5.8898 3.4449c-0.67467 1e-7 -1.2225 0.5478-1.2225 1.2225v14.665c0 0.67466 0.5478 1.2225 1.2225 1.2225h12.22c0.67466 0 1.2225-0.5478 1.2225-1.2225v-11h-1.8326c-1.6854 0-3.0551-1.3696-3.0551-3.0551v-1.8326zm9.7775 0.50488v1.3277c0 1.0108 0.82184 1.8326 1.8326 1.8326h1.3277z" fill="#ededed" stroke-linecap="round" stroke-width=".55"/>
<path d="m5.8898 1c-2.0216 0-3.6674 1.6458-3.6674 3.6674v14.665c0 2.0216 1.6458 3.6674 3.6674 3.6674h12.22c2.0216 0 3.6674-1.6458 3.6674-3.6674v-12.222c0-0.32511-0.12901-0.63512-0.35879-0.86367l-4.8877-4.8877c-0.22856-0.22978-0.53856-0.35879-0.86367-0.35879zm0 2.4449h8.5551v1.8326c0 1.6854 1.3696 3.0551 3.0551 3.0551h1.8326v11c0 0.67466-0.5478 1.2225-1.2225 1.2225h-12.22c-0.67467 0-1.2225-0.5478-1.2225-1.2225v-14.665c0-0.67467 0.5478-1.2225 1.2225-1.2225zm9.7775 0.50488 3.1604 3.1604h-1.3277c-1.0108 0-1.8326-0.82184-1.8326-1.8326z" fill="#333" stroke-width="1.2222"/>
<path d="m12 6c-0.82934 0-1.5 0.67081-1.5 1.5003v3.0007h-3c-0.82934 0-1.5 0.67081-1.5 1.5003 0 0.82952 0.67066 1.5003 1.5 1.5003h3v3.0007c0 0.82952 0.67066 1.4977 1.5 1.4977 0.82934 0 1.5-0.66817 1.5-1.4977v-3.0007h3c0.82934 0 1.5-0.67081 1.5-1.5003 1e-6 -0.82952-0.67066-1.5003-1.5-1.5003h-3v-3.0007c0-0.82952-0.67066-1.5003-1.5-1.5003z" fill="#4271ae" stroke-width="1.4999"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="#4271ae" stroke-width="1.0526">
<path d="m19.632 17.263h-7.3684c-1.1611 0-2.1053 0.94421-2.1053 2.1053s0.94421 2.1053 2.1053 2.1053h7.3684c1.1611 0 2.1053-0.94421 2.1053-2.1053s-0.94421-2.1053-2.1053-2.1053zm0-7.3684h-7.3684c-1.1611 0-2.1053 0.94421-2.1053 2.1053s0.94421 2.1053 2.1053 2.1053h7.3684c1.1611 0 2.1053-0.94421 2.1053-2.1053s-0.94421-2.1053-2.1053-2.1053zm0-7.3684h-7.3684c-1.1611 0-2.1053 0.94421-2.1053 2.1053 0 1.1611 0.94421 2.1053 2.1053 2.1053h7.3684c1.1611 0 2.1053-0.94421 2.1053-2.1053 0-1.1611-0.94421-2.1053-2.1053-2.1053z"/>
<circle cx="4.8947" cy="19.368" r="2.6316"/>
<circle cx="4.8947" cy="12" r="2.6316"/>
<circle cx="4.8947" cy="4.6316" r="2.6316"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 849 B

@@ -0,0 +1,26 @@
[Main]
name = Cyberpunk Night
author = Anders Lemvigh
url = https://github.com/alemvigh
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Syntax]
background = 0, 0, 0
text = 150, 150, 150
link = 77, 077, 255
headertext = 255, 255, 255
headertag = 50, 0, 180
emphasis = 0, 255, 255
straightquotes = 7, 27, 219
doublequotes = 0, 255, 0
singlequotes = 0, 140, 255
hidden = 77, 77, 100
shortcode = 255, 255, 0
keyword = 255, 100, 255
value = 255, 150, 10
spellcheckline = 242, 72, 23
errorline = 186, 218, 4
replacetag = 0, 0, 180
modifier = 144, 142, 176
optional = 180, 180, 180
+23
View File
@@ -0,0 +1,23 @@
[Main]
name = Tango
author = Veronica Berglyd Olsen (adaptation)
[Syntax]
background = 48, 48, 47
text = 238, 238, 236
link = 115, 159, 207
headertext = 115, 159, 207
headertag = 52, 101, 164
emphasis = 196, 160, 0
straightquotes = 239, 41, 41
doublequotes = 138, 226, 52
singlequotes = 252, 233, 79
hidden = 143, 143, 141
shortcode = 115, 159, 207
keyword = 239, 41, 41
value = 173, 127, 168
optional = 115, 159, 207
spellcheckline = 239, 41, 41
errorline = 138, 226, 52
replacetag = 51, 226, 226
modifier = 196, 160, 0
@@ -0,0 +1,29 @@
[Main]
name = Cyberpunk Night
description = A taste of the future 80s
author = Anders Lemvigh
url = https://github.com/alemvigh
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
icontheme = typicons_dark
[Palette]
window = 0, 0, 0
windowtext = 150, 150, 150
base = 0, 0, 0
alternatebase = 30, 20, 45
text = 150, 150, 150
tooltipbase = 40, 20, 70
tooltiptext = 255, 255, 255
button = 5, 0, 10
buttontext = 150, 150, 150
brighttext = 255, 255, 255
highlight = 50, 30, 80
highlightedtext = 255, 255, 255
link = 77, 77, 255
linkvisited = 50, 0, 80
[GUI]
statusnone = 50, 50, 50
statussaved = 77, 255, 77
statusunsaved = 255, 77, 77
+10 -8
View File
@@ -365,14 +365,10 @@ class ProjectBuilder:
lblByAuthors = self.tr("By") lblByAuthors = self.tr("By")
# Settings # Settings
projName = data.get("name", lblNewProject)
projAuthor = data.get("author", "")
projLang = data.get("language", "en_GB")
project.data.setUuid(None) project.data.setUuid(None)
project.data.setName(projName) project.data.setName(data.get("name", lblNewProject))
project.data.setAuthor(projAuthor) project.data.setAuthor(data.get("author", ""))
project.data.setLanguage(projLang) project.data.setLanguage(CONFIG.guiLocale)
project.setDefaultStatusImport() project.setDefaultStatusImport()
project.session.startSession() project.session.startSession()
@@ -502,7 +498,6 @@ class ProjectBuilder:
project.data.setUuid("") # Creates a fresh uuid project.data.setUuid("") # Creates a fresh uuid
project.data.setName(data.get("name", "None")) project.data.setName(data.get("name", "None"))
project.data.setAuthor(data.get("author", "")) project.data.setAuthor(data.get("author", ""))
project.data.setLanguage(data.get("language", "en_GB"))
project.data.setSpellCheck(True) project.data.setSpellCheck(True)
project.data.setSpellLang(None) project.data.setSpellLang(None)
project.data.setDoBackup(True) project.data.setDoBackup(True)
@@ -518,6 +513,13 @@ class ProjectBuilder:
"""Make a copy of the sample project by extracting the """Make a copy of the sample project by extracting the
sample.zip file to the new path. sample.zip file to the new path.
""" """
if path.exists():
SHARED.error(self.tr(
"The target folder already exists. "
"Please choose another folder."
))
return False
if (sample := CONFIG.assetPath("sample.zip")).is_file(): if (sample := CONFIG.assetPath("sample.zip")).is_file():
try: try:
shutil.unpack_archive(sample, path) shutil.unpack_archive(sample, path)
+1
View File
@@ -410,6 +410,7 @@ class NWProject:
def closeProject(self, idleTime: float = 0.0) -> None: def closeProject(self, idleTime: float = 0.0) -> None:
"""Close the project.""" """Close the project."""
logger.info("Closing project") logger.info("Closing project")
self._index.clearIndex() # Triggers clear signal, see #1718
self._options.saveSettings() self._options.saveSettings()
self._tree.writeToCFile() self._tree.writeToCFile()
self._session.appendSession(idleTime) self._session.appendSession(idleTime)
+8 -4
View File
@@ -183,11 +183,15 @@ class NWStorage:
# 2. A full path to an nwProject.nwx file # 2. A full path to an nwProject.nwx file
if inPath.is_dir() and inPath != Path.home().resolve(): if inPath.is_dir() and inPath != Path.home().resolve():
nwxFile = inPath / nwFiles.PROJ_FILE nwxFile = inPath / nwFiles.PROJ_FILE
elif inPath.is_file() and inPath.name == nwFiles.PROJ_FILE: elif inPath.is_file():
nwxFile = inPath if inPath.name == nwFiles.PROJ_FILE:
nwxFile = inPath
else:
logger.error("Not a novelWriter project")
return NWStorageOpen.UNKOWN
else: else:
logger.error("Not a novelWriter project") logger.error("Not found: %s", inPath)
return NWStorageOpen.UNKOWN return NWStorageOpen.NOT_FOUND
if not nwxFile.exists(): if not nwxFile.exists():
# The .nwx file must exist to continue # The .nwx file must exist to continue
+62 -63
View File
@@ -89,7 +89,7 @@ class Tokenizer(ABC):
T_UNNUM = 7 # Unnumbered T_UNNUM = 7 # Unnumbered
T_HEAD1 = 8 # Header 1 T_HEAD1 = 8 # Header 1
T_HEAD2 = 9 # Header 2 T_HEAD2 = 9 # Header 2
T_HEAD3 = 10 # Header 3 T_HEAD3 = 10 # Header 3
T_HEAD4 = 11 # Header 4 T_HEAD4 = 11 # Header 4
T_TEXT = 12 # Text line T_TEXT = 12 # Text line
T_SEP = 13 # Scene separator T_SEP = 13 # Scene separator
@@ -158,7 +158,7 @@ class Tokenizer(ABC):
# Instance Variables # Instance Variables
self._hFormatter = HeadingFormatter(self._project) self._hFormatter = HeadingFormatter(self._project)
self._firstScene = False # Flag to indicate that the first scene of the chapter self._skipSeparator = False # Flag to indicate that we skip the scene separator
# This File # This File
self._isNone = False # Document has unknown layout self._isNone = False # Document has unknown layout
@@ -615,9 +615,9 @@ class Tokenizer(ABC):
# Make sure the token array doesn't start with a page break # Make sure the token array doesn't start with a page break
# on the very first page, adding a blank first page. # on the very first page, adding a blank first page.
if self._tokens[0][4] & self.A_PBB: if self._tokens[0][4] & self.A_PBB:
tToken = self._tokens[0] token = self._tokens[0]
self._tokens[0] = ( self._tokens[0] = (
tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] & ~self.A_PBB token[0], token[1], token[2], token[3], token[4] & ~self.A_PBB
) )
# Always add an empty line at the end of the file # Always add an empty line at the end of the file
@@ -637,22 +637,20 @@ class Tokenizer(ABC):
pToken = (self.T_EMPTY, 0, "", [], self.A_NONE) pToken = (self.T_EMPTY, 0, "", [], self.A_NONE)
nToken = (self.T_EMPTY, 0, "", [], self.A_NONE) nToken = (self.T_EMPTY, 0, "", [], self.A_NONE)
tCount = len(self._tokens) tCount = len(self._tokens)
for n, tToken in enumerate(self._tokens): for n, token in enumerate(self._tokens):
if n > 0: if n > 0:
pToken = self._tokens[n-1] pToken = self._tokens[n-1]
if n < tCount - 1: if n < tCount - 1:
nToken = self._tokens[n+1] nToken = self._tokens[n+1]
if tToken[0] == self.T_KEYWORD: if token[0] == self.T_KEYWORD:
aStyle = tToken[4] aStyle = token[4]
if pToken[0] == self.T_KEYWORD: if pToken[0] == self.T_KEYWORD:
aStyle |= self.A_Z_TOPMRG aStyle |= self.A_Z_TOPMRG
if nToken[0] == self.T_KEYWORD: if nToken[0] == self.T_KEYWORD:
aStyle |= self.A_Z_BTMMRG aStyle |= self.A_Z_BTMMRG
self._tokens[n] = ( self._tokens[n] = (token[0], token[1], token[2], token[3], aStyle)
tToken[0], tToken[1], tToken[2], tToken[3], aStyle
)
return return
@@ -665,93 +663,87 @@ class Tokenizer(ABC):
self._hFormatter.setHandle(self._nwItem.itemHandle if self._nwItem else None) self._hFormatter.setHandle(self._nwItem.itemHandle if self._nwItem else None)
for n, tToken in enumerate(self._tokens): for n, token in enumerate(self._tokens):
# In case we see text before a scene, we reset the flag if token[0] == self.T_TITLE: # Title
if tToken[0] == self.T_TEXT: # For new titles, we reset all counters
self._firstScene = False self._skipSeparator = True
self._hFormatter.resetAll()
elif tToken[0] == self.T_HEAD1: elif token[0] == self.T_HEAD1: # Partition
# Partition
tTemp = self._hFormatter.apply(self._fmtTitle, tToken[2], tToken[1]) tTemp = self._hFormatter.apply(self._fmtTitle, token[2], token[1])
self._tokens[n] = ( self._tokens[n] = (
tToken[0], tToken[1], tTemp, [], tToken[4] token[0], token[1], tTemp, [], token[4]
)
elif tToken[0] in (self.T_HEAD2, self.T_UNNUM):
# Chapter
# Numbered or Unnumbered
if tToken[0] == self.T_UNNUM:
tTemp = self._hFormatter.apply(self._fmtUnNum, tToken[2], tToken[1])
else:
self._hFormatter.incChapter()
tTemp = self._hFormatter.apply(self._fmtChapter, tToken[2], tToken[1])
# Format the chapter header
self._tokens[n] = (
tToken[0], tToken[1], tTemp, [], tToken[4]
) )
# Set scene variables # Set scene variables
self._firstScene = True self._skipSeparator = True
self._hFormatter.resetScene() self._hFormatter.resetScene()
elif tToken[0] == self.T_HEAD3: elif token[0] in (self.T_HEAD2, self.T_UNNUM): # Chapter, Unnumbered
# Scene
# Numbered or Unnumbered
if token[0] == self.T_UNNUM:
tTemp = self._hFormatter.apply(self._fmtUnNum, token[2], token[1])
else:
self._hFormatter.incChapter()
tTemp = self._hFormatter.apply(self._fmtChapter, token[2], token[1])
# Format the chapter header
self._tokens[n] = (
token[0], token[1], tTemp, [], token[4]
)
# Set scene variables
self._skipSeparator = True
self._hFormatter.resetScene()
elif token[0] == self.T_HEAD3: # Scene
self._hFormatter.incScene() self._hFormatter.incScene()
tTemp = self._hFormatter.apply(self._fmtScene, tToken[2], tToken[1]) tTemp = self._hFormatter.apply(self._fmtScene, token[2], token[1])
if tTemp == "" and self._hideScene: if tTemp == "" and self._hideScene:
self._tokens[n] = ( self._tokens[n] = (
self.T_EMPTY, tToken[1], "", [], self.A_NONE self.T_EMPTY, token[1], "", [], self.A_NONE
) )
elif tTemp == "" and not self._hideScene: elif tTemp == "" and not self._hideScene:
if self._firstScene: self._tokens[n] = (
self._tokens[n] = ( self.T_EMPTY if self._skipSeparator else self.T_SKIP, token[1],
self.T_EMPTY, tToken[1], "", [], self.A_NONE "", [], self.A_NONE if self._skipSeparator else token[4]
) )
else:
self._tokens[n] = (
self.T_SKIP, tToken[1], "", [], tToken[4]
)
elif tTemp == self._fmtScene: elif tTemp == self._fmtScene:
if self._firstScene: self._tokens[n] = (
self._tokens[n] = ( self.T_EMPTY if self._skipSeparator else self.T_SEP, token[1],
self.T_EMPTY, tToken[1], "", [], self.A_NONE "" if self._skipSeparator else tTemp, [],
) self.A_NONE if self._skipSeparator else (token[4] | self.A_CENTRE)
else: )
self._tokens[n] = (
self.T_SEP, tToken[1], tTemp, [], tToken[4] | self.A_CENTRE
)
else: else:
self._tokens[n] = ( self._tokens[n] = (
tToken[0], tToken[1], tTemp, [], tToken[4] token[0], token[1], tTemp, [], token[4]
) )
self._firstScene = False self._skipSeparator = False
elif tToken[0] == self.T_HEAD4: elif token[0] == self.T_HEAD4: # Section
# Section
tTemp = self._hFormatter.apply(self._fmtSection, tToken[2], tToken[1]) tTemp = self._hFormatter.apply(self._fmtSection, token[2], token[1])
if tTemp == "" and self._hideSection: if tTemp == "" and self._hideSection:
self._tokens[n] = ( self._tokens[n] = (
self.T_EMPTY, tToken[1], "", [], self.A_NONE self.T_EMPTY, token[1], "", [], self.A_NONE
) )
elif tTemp == "" and not self._hideSection: elif tTemp == "" and not self._hideSection:
self._tokens[n] = ( self._tokens[n] = (
self.T_SKIP, tToken[1], "", [], tToken[4] self.T_SKIP, token[1], "", [], token[4]
) )
elif tTemp == self._fmtSection: elif tTemp == self._fmtSection:
self._tokens[n] = ( self._tokens[n] = (
self.T_SEP, tToken[1], tTemp, [], tToken[4] | self.A_CENTRE self.T_SEP, token[1], tTemp, [], token[4] | self.A_CENTRE
) )
else: else:
self._tokens[n] = ( self._tokens[n] = (
tToken[0], tToken[1], tTemp, [], tToken[4] token[0], token[1], tTemp, [], token[4]
) )
return True return True
@@ -849,6 +841,13 @@ class HeadingFormatter:
self._scAbsCount += 1 self._scAbsCount += 1
return return
def resetAll(self) -> None:
"""Reset all counters."""
self._chCount = 0
self._scChCount = 0
self._scAbsCount = 0
return
def resetScene(self) -> None: def resetScene(self) -> None:
"""Reset the chapter scene counter.""" """Reset the chapter scene counter."""
self._scChCount = 0 self._scChCount = 0
+2 -2
View File
@@ -68,9 +68,9 @@ class GuiAbout(QDialog):
self.nwInfo = VersionInfoWidget(self) self.nwInfo = VersionInfoWidget(self)
self.nwLicence = QLabel(self.tr("This application is licenced under {0}".format( self.nwLicence = QLabel(self.tr("This application is licenced under {0}").format(
"<a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a>" "<a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a>"
))) ))
self.nwLicence.setOpenExternalLinks(True) self.nwLicence.setOpenExternalLinks(True)
# Credits # Credits
+53 -45
View File
@@ -505,10 +505,15 @@ class GuiProjectTree(QTreeWidget):
self.mainGui = projView.mainGui self.mainGui = projView.mainGui
# Internal Variables # Internal Variables
self._treeMap = {} self._treeMap: dict[str, QTreeWidgetItem] = {}
self._timeChanged = 0.0 self._timeChanged = 0.0
self._popAlert = None self._popAlert = None
# Cached Translations
self.trActive = self.tr("Active")
self.trInactive = self.tr("Inactive")
self.trPermDelete = self.tr("Permanently delete {0} file(s) from Trash?")
# Build GUI # Build GUI
# ========= # =========
@@ -555,10 +560,6 @@ class GuiProjectTree(QTreeWidget):
trRoot = self.invisibleRootItem() trRoot = self.invisibleRootItem()
trRoot.setFlags(trRoot.flags() ^ Qt.ItemFlag.ItemIsDropEnabled) trRoot.setFlags(trRoot.flags() ^ Qt.ItemFlag.ItemIsDropEnabled)
# Cached values
self._lblActive = self.tr("Active")
self._lblInactive = self.tr("Inactive")
# Set selection options # Set selection options
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
@@ -902,10 +903,7 @@ class GuiProjectTree(QTreeWidget):
SHARED.info(self.tr("The Trash folder is already empty.")) SHARED.info(self.tr("The Trash folder is already empty."))
return False return False
msgYes = SHARED.question( if not SHARED.question(self.trPermDelete.format(nTrash)):
self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash)
)
if not msgYes:
logger.info("Action cancelled by user") logger.info("Action cancelled by user")
return False return False
@@ -1052,7 +1050,7 @@ class GuiProjectTree(QTreeWidget):
if nwItem.isFileType(): if nwItem.isFileType():
iconName = "checked" if nwItem.isActive else "unchecked" iconName = "checked" if nwItem.isActive else "unchecked"
toolTip = self._lblActive if nwItem.isActive else self._lblInactive toolTip = self.trActive if nwItem.isActive else self.trInactive
trItem.setToolTip(self.C_ACTIVE, toolTip) trItem.setToolTip(self.C_ACTIVE, toolTip)
else: else:
iconName = "noncheckable" iconName = "noncheckable"
@@ -1129,9 +1127,8 @@ class GuiProjectTree(QTreeWidget):
"""Get the currently selected handle. If multiple items are """Get the currently selected handle. If multiple items are
selected, return the first. selected, return the first.
""" """
selItem = self.selectedItems() if items := self.selectedItems():
if selItem: return items[0].data(self.C_DATA, self.D_HANDLE)
return selItem[0].data(self.C_DATA, self.D_HANDLE)
return None return None
def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> bool: def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> bool:
@@ -1143,9 +1140,8 @@ class GuiProjectTree(QTreeWidget):
if tHandle in self._treeMap: if tHandle in self._treeMap:
self.setCurrentItem(self._treeMap[tHandle]) self.setCurrentItem(self._treeMap[tHandle])
selIndex = self.selectedIndexes() if (indexes := self.selectedIndexes()) and doScroll:
if selIndex and doScroll: self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter)
self.scrollTo(selIndex[0], QAbstractItemView.ScrollHint.PositionAtCenter)
return True return True
@@ -1160,10 +1156,8 @@ class GuiProjectTree(QTreeWidget):
def openContextOnSelected(self) -> bool: def openContextOnSelected(self) -> bool:
"""Open the context menu on the current selected item.""" """Open the context menu on the current selected item."""
selItem = self.selectedItems() if items := self.selectedItems():
if selItem: return self._openContextMenu(self.visualItemRect(items[0]).center())
pos = self.visualItemRect(selItem[0]).center()
return self._openContextMenu(pos)
return False return False
def changedSince(self, checkTime: float) -> bool: def changedSince(self, checkTime: float) -> bool:
@@ -1746,7 +1740,7 @@ class _TreeContextMenu(QMenu):
self._item = nwItem self._item = nwItem
self._handle = nwItem.itemHandle self._handle = nwItem.itemHandle
self._items: list[str] = [] self._items: list[NWItem] = []
logger.debug("Ready: _TreeContextMenu") logger.debug("Ready: _TreeContextMenu")
@@ -1799,13 +1793,17 @@ class _TreeContextMenu(QMenu):
return return
def buildMultiSelectMenu(self, items: list[str]) -> None: def buildMultiSelectMenu(self, handles: list[str]) -> None:
"""Build the multi-select menu.""" """Build the multi-select menu."""
self._items = items self._items = []
for tHandle in handles:
if (tItem := SHARED.project.tree[tHandle]):
self._items.append(tItem)
self._itemActive(True) self._itemActive(True)
self._itemStatusImport(True) self._itemStatusImport(True)
self.addSeparator() self.addSeparator()
self._moveToTrash(True) self._multiMoveToTrash()
return return
## ##
@@ -1847,9 +1845,9 @@ class _TreeContextMenu(QMenu):
"""Add Active/Inactive actions.""" """Add Active/Inactive actions."""
if multi: if multi:
mSub = self.addMenu(self.tr("Set Active to ...")) mSub = self.addMenu(self.tr("Set Active to ..."))
aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.tr("Active")) aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.projTree.trActive)
aOne.triggered.connect(lambda: self._iterItemActive(True)) aOne.triggered.connect(lambda: self._iterItemActive(True))
aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.tr("Inactive")) aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.projTree.trInactive)
aTwo.triggered.connect(lambda: self._iterItemActive(False)) aTwo.triggered.connect(lambda: self._iterItemActive(False))
else: else:
action = self.addAction(self.tr("Toggle Active")) action = self.addAction(self.tr("Toggle Active"))
@@ -1964,10 +1962,14 @@ class _TreeContextMenu(QMenu):
return return
def _moveToTrash(self, multi: bool) -> None: def _multiMoveToTrash(self) -> None:
"""Add move to Trash action.""" """Add move to Trash action."""
action = self.addAction(self.tr("Move to Trash")) areTrash = [i.itemClass == nwItemClass.TRASH for i in self._items]
if multi: if all(areTrash):
action = self.addAction(self.tr("Delete Permanently"))
action.triggered.connect(self._iterPermDelete)
elif not any(areTrash):
action = self.addAction(self.tr("Move to Trash"))
action.triggered.connect(self._iterMoveToTrash) action.triggered.connect(self._iterMoveToTrash)
return return
@@ -1979,10 +1981,19 @@ class _TreeContextMenu(QMenu):
def _iterMoveToTrash(self) -> None: def _iterMoveToTrash(self) -> None:
"""Iterate through files and move them to Trash.""" """Iterate through files and move them to Trash."""
if SHARED.question(self.tr("Move {0} items to Trash?").format(len(self._items))): if SHARED.question(self.tr("Move {0} items to Trash?").format(len(self._items))):
for tHandle in self._items: for tItem in self._items:
tItem = SHARED.project.tree[tHandle] if tItem.isFileType() and tItem.itemClass != nwItemClass.TRASH:
if tItem and tItem.isFileType(): self.projTree.moveItemToTrash(tItem.itemHandle, askFirst=False, flush=False)
self.projTree.moveItemToTrash(tHandle, askFirst=False, flush=False) self.projTree.saveTreeOrder()
return
@pyqtSlot()
def _iterPermDelete(self) -> None:
"""Iterate through files and delete them."""
if SHARED.question(self.projTree.trPermDelete.format(len(self._items))):
for tItem in self._items:
if tItem.isFileType() and tItem.itemClass == nwItemClass.TRASH:
self.projTree.permDeleteItem(tItem.itemHandle, askFirst=False, flush=False)
self.projTree.saveTreeOrder() self.projTree.saveTreeOrder()
return return
@@ -2000,12 +2011,11 @@ class _TreeContextMenu(QMenu):
def _iterItemActive(self, isActive: bool) -> None: def _iterItemActive(self, isActive: bool) -> None:
"""Set the active status of multiple items.""" """Set the active status of multiple items."""
for tHandle in self._items: for tItem in self._items:
tItem = SHARED.project.tree[tHandle]
if tItem and tItem.isFileType(): if tItem and tItem.isFileType():
tItem.setActive(isActive) tItem.setActive(isActive)
self.projTree.setTreeItemValues(tHandle) self.projTree.setTreeItemValues(tItem.itemHandle)
self.projTree._alertTreeChange(tHandle, flush=False) self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
return return
def _changeItemStatus(self, key: str) -> None: def _changeItemStatus(self, key: str) -> None:
@@ -2017,12 +2027,11 @@ class _TreeContextMenu(QMenu):
def _iterSetItemStatus(self, key: str) -> None: def _iterSetItemStatus(self, key: str) -> None:
"""Change the status value for multiple items.""" """Change the status value for multiple items."""
for tHandle in self._items: for tItem in self._items:
tItem = SHARED.project.tree[tHandle]
if tItem and tItem.isNovelLike(): if tItem and tItem.isNovelLike():
tItem.setStatus(key) tItem.setStatus(key)
self.projTree.setTreeItemValues(tHandle) self.projTree.setTreeItemValues(tItem.itemHandle)
self.projTree._alertTreeChange(tHandle, flush=False) self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
return return
def _changeItemImport(self, key: str) -> None: def _changeItemImport(self, key: str) -> None:
@@ -2034,12 +2043,11 @@ class _TreeContextMenu(QMenu):
def _iterSetItemImport(self, key: str) -> None: def _iterSetItemImport(self, key: str) -> None:
"""Change the status value for multiple items.""" """Change the status value for multiple items."""
for tHandle in self._items: for tItem in self._items:
tItem = SHARED.project.tree[tHandle]
if tItem and not tItem.isNovelLike(): if tItem and not tItem.isNovelLike():
tItem.setImport(key) tItem.setImport(key)
self.projTree.setTreeItemValues(tHandle) self.projTree.setTreeItemValues(tItem.itemHandle)
self.projTree._alertTreeChange(tHandle, flush=False) self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
return return
def _changeItemLayout(self, itemLayout: nwItemLayout) -> None: def _changeItemLayout(self, itemLayout: nwItemLayout) -> None:
+12 -5
View File
@@ -344,7 +344,7 @@ class GuiTheme:
if themeName: if themeName:
self._themeList.append((themeKey, themeName)) self._themeList.append((themeKey, themeName))
self._themeList = sorted(self._themeList, key=lambda x: x[1]) self._themeList = sorted(self._themeList, key=_sortTheme)
return self._themeList return self._themeList
@@ -360,7 +360,7 @@ class GuiTheme:
if syntaxName: if syntaxName:
self._syntaxList.append((syntaxKey, syntaxName)) self._syntaxList.append((syntaxKey, syntaxName))
self._syntaxList = sorted(self._syntaxList, key=lambda x: x[1]) self._syntaxList = sorted(self._syntaxList, key=_sortTheme)
return self._syntaxList return self._syntaxList
@@ -453,9 +453,9 @@ class GuiIcons:
# General Button Icons # General Button Icons
"add", "add_document", "backward", "bookmark", "browse", "checked", "close", "cross", "add", "add_document", "backward", "bookmark", "browse", "checked", "close", "cross",
"document", "down", "edit", "export", "forward", "import", "maximise", "menu", "minimise", "document", "down", "edit", "export", "forward", "import", "list", "maximise", "menu",
"more", "noncheckable", "panel", "refresh", "remove", "revert", "search_replace", "search", "minimise", "more", "noncheckable", "open", "panel", "refresh", "remove", "revert",
"settings", "star", "unchecked", "up", "view", "search_replace", "search", "settings", "star", "unchecked", "up", "view",
# Switches # Switches
"sticky-on", "sticky-off", "sticky-on", "sticky-off",
@@ -725,6 +725,13 @@ class GuiIcons:
# Module Functions # Module Functions
# =============================================================================================== # # =============================================================================================== #
def _sortTheme(data: tuple[str, str]) -> str:
"""Key function for theme sorting."""
key, name = data
return f"*{name}" if key.startswith("default_") else name
def _loadInternalName(confParser: NWConfigParser, confFile: str | Path) -> str: def _loadInternalName(confParser: NWConfigParser, confFile: str | Path) -> str:
"""Open a conf file and read the 'name' setting.""" """Open a conf file and read the 'name' setting."""
try: try:
+5 -3
View File
@@ -796,7 +796,7 @@ class GuiMain(QMainWindow):
def showWelcomeDialog(self) -> None: def showWelcomeDialog(self) -> None:
"""Open the welcome dialog.""" """Open the welcome dialog."""
dialog = GuiWelcome(self) dialog = GuiWelcome(self)
dialog.openProjectRequest.connect(self._openProject) dialog.openProjectRequest.connect(self._openProjectFromWelcome)
dialog.exec_() dialog.exec_()
return return
@@ -1132,10 +1132,12 @@ class GuiMain(QMainWindow):
return return
@pyqtSlot(Path) @pyqtSlot(Path)
def _openProject(self, path: Path) -> None: def _openProjectFromWelcome(self, path: Path) -> None:
"""Handle an open project request.""" """Handle an open project request from the welcome dialog."""
qApp.processEvents() qApp.processEvents()
self.openProject(path) self.openProject(path)
if not SHARED.hasProject:
self.showWelcomeDialog()
return return
@pyqtSlot(str, nwDocMode, str, bool) @pyqtSlot(str, nwDocMode, str, bool)
+1 -1
View File
@@ -216,7 +216,7 @@ class _OverviewPage(NScrollablePage):
self.projForm.addRow("<b>{0}</b>".format(self.tr("Editing Time")), self.projEditTime) self.projForm.addRow("<b>{0}</b>".format(self.tr("Editing Time")), self.projEditTime)
self.projForm.addRow("<b>{0}</b>".format(self.tr("Word Count")), self.projWords) self.projForm.addRow("<b>{0}</b>".format(self.tr("Word Count")), self.projWords)
self.projForm.addRow("<b>\u2026 {0}</b>".format(self.tr("In Novels")), self.projNovels) self.projForm.addRow("<b>\u2026 {0}</b>".format(self.tr("In Novels")), self.projNovels)
self.projForm.addRow("<b>\u2026 {0}</b>".format(self.tr("In Notes ")), self.projNotes) self.projForm.addRow("<b>\u2026 {0}</b>".format(self.tr("In Notes")), self.projNotes)
self.projForm.setContentsMargins(mPx, 0, 0, 0) self.projForm.setContentsMargins(mPx, 0, 0, 0)
self.projForm.setHorizontalSpacing(hPx) self.projForm.setHorizontalSpacing(hPx)
self.projForm.setVerticalSpacing(vPx) self.projForm.setVerticalSpacing(vPx)
+78 -61
View File
@@ -34,10 +34,10 @@ from PyQt5.QtCore import (
pyqtSignal, pyqtSlot pyqtSignal, pyqtSlot
) )
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAction, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QHBoxLayout, QAction, QDialog, QFileDialog, QFormLayout, QHBoxLayout, QLabel, QLineEdit,
QLabel, QLineEdit, QListView, QMenu, QPushButton, QScrollArea, QShortcut, QListView, QMenu, QPushButton, QScrollArea, QShortcut, QStackedWidget,
QStackedWidget, QStyle, QStyleOptionViewItem, QStyledItemDelegate, QStyle, QStyleOptionViewItem, QStyledItemDelegate, QToolButton,
QToolButton, QVBoxLayout, QWidget, qApp QVBoxLayout, QWidget, qApp
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
@@ -46,7 +46,7 @@ from novelwriter.common import formatInt, makeFileNameSafe
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.coretools import ProjectBuilder from novelwriter.core.coretools import ProjectBuilder
from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.modified import NComboBox, NSpinBox from novelwriter.extensions.modified import NSpinBox
from novelwriter.extensions.versioninfo import VersionInfoWidget from novelwriter.extensions.versioninfo import VersionInfoWidget
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -97,7 +97,6 @@ class GuiWelcome(QDialog):
self.tabOpen.openProjectRequest.connect(self._openProjectPath) self.tabOpen.openProjectRequest.connect(self._openProjectPath)
self.tabNew = _NewProjectPage(self) self.tabNew = _NewProjectPage(self)
self.tabNew.cancelNewProject.connect(self._showOpenProjectPage)
self.tabNew.openProjectRequest.connect(self._openProjectPath) self.tabNew.openProjectRequest.connect(self._openProjectPath)
self.mainStack = QStackedWidget(self) self.mainStack = QStackedWidget(self)
@@ -107,17 +106,39 @@ class GuiWelcome(QDialog):
# Buttons # Buttons
# ======= # =======
self.btnBox = QDialogButtonBox(QDialogButtonBox.Open | QDialogButtonBox.Cancel, self) self.btnList = QPushButton(self.tr("List"), self)
self.btnBox.accepted.connect(self.tabOpen.openSelectedItem) self.btnList.setIcon(SHARED.theme.getIcon("list"))
self.btnBox.rejected.connect(self.close) self.btnList.clicked.connect(self._showOpenProjectPage)
self.newButton = self.btnBox.addButton(self.tr("New Project"), QDialogButtonBox.ActionRole) self.btnNew = QPushButton(self.tr("New"), self)
self.newButton.setIcon(SHARED.theme.getIcon("add")) self.btnNew.setIcon(SHARED.theme.getIcon("add"))
self.newButton.clicked.connect(self._showNewProjectPage) self.btnNew.clicked.connect(self._showNewProjectPage)
self.browseButton = self.btnBox.addButton(self.tr("Browse"), QDialogButtonBox.ActionRole) self.btnBrowse = QPushButton(self.tr("Browse"), self)
self.browseButton.setIcon(SHARED.theme.getIcon("browse")) self.btnBrowse.setIcon(SHARED.theme.getIcon("browse"))
self.browseButton.clicked.connect(self._browseForProject) self.btnBrowse.clicked.connect(self._browseForProject)
self.btnCancel = QPushButton(self.tr("Cancel"), self)
self.btnCancel.setIcon(SHARED.theme.getIcon("cross"))
self.btnCancel.clicked.connect(self.close)
self.btnCreate = QPushButton(self.tr("Create"), self)
self.btnCreate.setIcon(SHARED.theme.getIcon("star"))
self.btnCreate.clicked.connect(self.tabNew.createNewProject)
self.btnOpen = QPushButton(self.tr("Open"), self)
self.btnOpen.setIcon(SHARED.theme.getIcon("open"))
self.btnOpen.clicked.connect(self._openSelectedItem)
self.btnBox = QHBoxLayout()
self.btnBox.addStretch(1)
self.btnBox.addWidget(self.btnList)
self.btnBox.addWidget(self.btnNew)
self.btnBox.addWidget(self.btnBrowse)
self.btnBox.addWidget(self.btnCancel)
self.btnBox.addWidget(self.btnCreate)
self.btnBox.addWidget(self.btnOpen)
self._setButtonVisibility()
# Assemble # Assemble
# ======== # ========
@@ -129,7 +150,7 @@ class GuiWelcome(QDialog):
self.innerBox.addSpacing(hA) self.innerBox.addSpacing(hA)
self.innerBox.addWidget(self.mainStack) self.innerBox.addWidget(self.mainStack)
self.innerBox.addSpacing(hB) self.innerBox.addSpacing(hB)
self.innerBox.addWidget(self.btnBox) self.innerBox.addLayout(self.btnBox)
topRight = Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight topRight = Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight
@@ -179,12 +200,14 @@ class GuiWelcome(QDialog):
def _showNewProjectPage(self) -> None: def _showNewProjectPage(self) -> None:
"""Show the create new project page.""" """Show the create new project page."""
self.mainStack.setCurrentWidget(self.tabNew) self.mainStack.setCurrentWidget(self.tabNew)
self._setButtonVisibility()
return return
@pyqtSlot() @pyqtSlot()
def _showOpenProjectPage(self) -> None: def _showOpenProjectPage(self) -> None:
"""Show the open exiting project page.""" """Show the open exiting project page."""
self.mainStack.setCurrentWidget(self.tabOpen) self.mainStack.setCurrentWidget(self.tabOpen)
self._setButtonVisibility()
return return
@pyqtSlot() @pyqtSlot()
@@ -195,6 +218,13 @@ class GuiWelcome(QDialog):
self._openProjectPath(path) self._openProjectPath(path)
return return
@pyqtSlot()
def _openSelectedItem(self) -> None:
"""Open the currently selected project item."""
if self.mainStack.currentWidget() == self.tabOpen:
self.tabOpen.openSelectedItem()
return
@pyqtSlot(Path) @pyqtSlot(Path)
def _openProjectPath(self, path: Path) -> None: def _openProjectPath(self, path: Path) -> None:
"""Emit a project open signal.""" """Emit a project open signal."""
@@ -216,6 +246,20 @@ class GuiWelcome(QDialog):
CONFIG.setWelcomeWinSize(self.width(), self.height()) CONFIG.setWelcomeWinSize(self.width(), self.height())
return return
def _setButtonVisibility(self) -> None:
"""Change the visibility of the dialog buttons."""
listMode = self.mainStack.currentWidget() == self.tabOpen
self.btnList.setVisible(not listMode)
self.btnNew.setVisible(listMode)
self.btnBrowse.setVisible(listMode)
self.btnCreate.setVisible(not listMode)
self.btnOpen.setVisible(listMode)
if listMode:
self.btnOpen.setFocus()
else:
self.btnCreate.setFocus()
return
# END Class GuiWelcome # END Class GuiWelcome
@@ -445,7 +489,6 @@ class _ProjectListModel(QAbstractListModel):
class _NewProjectPage(QWidget): class _NewProjectPage(QWidget):
cancelNewProject = pyqtSignal()
openProjectRequest = pyqtSignal(Path) openProjectRequest = pyqtSignal(Path)
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
@@ -462,28 +505,11 @@ class _NewProjectPage(QWidget):
self.scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
# Controls
# ========
self.cancelButton = QPushButton(self.tr("Go Back"), self)
self.cancelButton.setIcon(SHARED.theme.getIcon("backward"))
self.cancelButton.clicked.connect(lambda: self.cancelNewProject.emit())
self.createButton = QPushButton(self.tr("Create Project"), self)
self.createButton.setIcon(SHARED.theme.getIcon("star"))
self.createButton.clicked.connect(self._createNewProject)
self.buttonBox = QHBoxLayout()
self.buttonBox.addStretch(1)
self.buttonBox.addWidget(self.cancelButton, 0)
self.buttonBox.addWidget(self.createButton, 0)
# Assemble # Assemble
# ======== # ========
self.outerBox = QVBoxLayout() self.outerBox = QVBoxLayout()
self.outerBox.addWidget(self.scrollArea) self.outerBox.addWidget(self.scrollArea)
self.outerBox.addLayout(self.buttonBox)
self.outerBox.setContentsMargins(0, 0, 0, 0) self.outerBox.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
@@ -500,11 +526,11 @@ class _NewProjectPage(QWidget):
return return
## ##
# Private Slots # Public Slots
## ##
@pyqtSlot() @pyqtSlot()
def _createNewProject(self) -> None: def createNewProject(self) -> None:
"""Create a new project from the data in the form.""" """Create a new project from the data in the form."""
data = self.projectForm.getProjectData() data = self.projectForm.getProjectData()
if not data.get("name"): if not data.get("name"):
@@ -532,6 +558,7 @@ class _NewProjectForm(QWidget):
self._copyPath = None self._copyPath = None
iPx = SHARED.theme.baseIconSize iPx = SHARED.theme.baseIconSize
sPx = CONFIG.pxInt(16)
# Project Settings # Project Settings
# ================ # ================
@@ -547,15 +574,6 @@ class _NewProjectForm(QWidget):
self.projAuthor.setMaxLength(200) self.projAuthor.setMaxLength(200)
self.projAuthor.setPlaceholderText(self.tr("Optional")) self.projAuthor.setPlaceholderText(self.tr("Optional"))
# Project Language
self.projLang = NComboBox(self)
for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ):
self.projLang.addItem(language, tag)
langIdx = self.projLang.findData(CONFIG.guiLocale)
if langIdx != -1:
self.projLang.setCurrentIndex(langIdx)
# Project Path # Project Path
self.projPath = QLineEdit(self) self.projPath = QLineEdit(self)
self.projPath.setReadOnly(True) self.projPath.setReadOnly(True)
@@ -601,7 +619,6 @@ class _NewProjectForm(QWidget):
self.projectForm.setAlignment(Qt.AlignmentFlag.AlignLeft) self.projectForm.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.projectForm.addRow(self.tr("Project Name"), self.projName) self.projectForm.addRow(self.tr("Project Name"), self.projName)
self.projectForm.addRow(self.tr("Author"), self.projAuthor) self.projectForm.addRow(self.tr("Author"), self.projAuthor)
self.projectForm.addRow(self.tr("Language"), self.projLang)
self.projectForm.addRow(self.tr("Project Path"), self.pathBox) self.projectForm.addRow(self.tr("Project Path"), self.pathBox)
self.projectForm.addRow(self.tr("Prefill Project"), self.fillBox) self.projectForm.addRow(self.tr("Prefill Project"), self.fillBox)
@@ -661,15 +678,23 @@ class _NewProjectForm(QWidget):
# Assemble # Assemble
# ======== # ========
self.extraBox = QVBoxLayout()
self.extraBox.addWidget(QLabel("<b>{0}</b>".format(self.tr("Chapters and Scenes"))))
self.extraBox.addLayout(self.novelForm)
self.extraBox.addSpacing(sPx)
self.extraBox.addWidget(QLabel("<b>{0}</b>".format(self.tr("Project Notes"))))
self.extraBox.addLayout(self.notesForm)
self.extraBox.setContentsMargins(0, 0, 0, 0)
self.extraWidget = QWidget(self)
self.extraWidget.setLayout(self.extraBox)
self.extraWidget.setContentsMargins(0, 0, 0, 0)
self.formBox = QVBoxLayout() self.formBox = QVBoxLayout()
self.formBox.addWidget(QLabel("<b>{0}</b>".format(self.tr("Create New Project")))) self.formBox.addWidget(QLabel("<b>{0}</b>".format(self.tr("Create New Project"))))
self.formBox.addLayout(self.projectForm) self.formBox.addLayout(self.projectForm)
self.formBox.addSpacing(16) self.formBox.addSpacing(sPx)
self.formBox.addWidget(QLabel("<b>{0}</b>".format(self.tr("Chapters and Scenes")))) self.formBox.addWidget(self.extraWidget)
self.formBox.addLayout(self.novelForm)
self.formBox.addSpacing(16)
self.formBox.addWidget(QLabel("<b>{0}</b>".format(self.tr("Project Notes"))))
self.formBox.addLayout(self.notesForm)
self.formBox.addStretch(1) self.formBox.addStretch(1)
self.setLayout(self.formBox) self.setLayout(self.formBox)
@@ -691,7 +716,6 @@ class _NewProjectForm(QWidget):
return { return {
"name": self.projName.text().strip(), "name": self.projName.text().strip(),
"author": self.projAuthor.text().strip(), "author": self.projAuthor.text().strip(),
"language": self.projLang.currentData(),
"path": self.projPath.text(), "path": self.projPath.text(),
"blank": self._fillMode == self.FILL_BLANK, "blank": self._fillMode == self.FILL_BLANK,
"sample": self._fillMode == self.FILL_SAMPLE, "sample": self._fillMode == self.FILL_SAMPLE,
@@ -775,14 +799,7 @@ class _NewProjectForm(QWidget):
self.projFill.setText(text) self.projFill.setText(text)
self.projFill.setToolTip(text) self.projFill.setToolTip(text)
self.projFill.setCursorPosition(0) self.projFill.setCursorPosition(0)
self.extraWidget.setVisible(self._fillMode == self.FILL_BLANK)
isBlank = self._fillMode == self.FILL_BLANK
self.numChapters.setEnabled(isBlank)
self.numScenes.setEnabled(isBlank)
self.addPlot.setEnabled(isBlank)
self.addChar.setEnabled(isBlank)
self.addWorld.setEnabled(isBlank)
self.addNotes.setEnabled(isBlank)
return return
-2
View File
@@ -41,8 +41,6 @@ install_requires =
include = novelwriter* include = novelwriter*
[options.entry_points] [options.entry_points]
console_script =
novelwriter-cli = novelwriter:main
gui_scripts = gui_scripts =
novelwriter = novelwriter:main novelwriter = novelwriter:main
@@ -1,12 +1,12 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.3a1" hexVersion="0x020300a1" fileVersion="1.5" fileRevision="2" timeStamp="2024-01-26 22:53:40"> <novelWriterXML appVersion="2.3b1" hexVersion="0x020300b1" fileVersion="1.5" fileRevision="2" timeStamp="2024-02-20 12:52:16">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="0" editTime="0"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="0" editTime="0">
<name>Test Project A</name> <name>Test Project A</name>
<author>Jane Doe</author> <author>Jane Doe</author>
</project> </project>
<settings> <settings>
<doBackup>yes</doBackup> <doBackup>yes</doBackup>
<language>None</language> <language>en_GB</language>
<spellChecking auto="no">None</spellChecking> <spellChecking auto="no">None</spellChecking>
<lastHandle> <lastHandle>
<entry key="editor">None</entry> <entry key="editor">None</entry>
@@ -1,12 +1,12 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.3a1" hexVersion="0x020300a1" fileVersion="1.5" fileRevision="2" timeStamp="2024-01-26 22:53:40"> <novelWriterXML appVersion="2.3b1" hexVersion="0x020300b1" fileVersion="1.5" fileRevision="2" timeStamp="2024-02-20 12:52:16">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="0" editTime="0"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="0" editTime="0">
<name>Test Project B</name> <name>Test Project B</name>
<author>Jane Doe</author> <author>Jane Doe</author>
</project> </project>
<settings> <settings>
<doBackup>yes</doBackup> <doBackup>yes</doBackup>
<language>None</language> <language>en_GB</language>
<spellChecking auto="no">None</spellChecking> <spellChecking auto="no">None</spellChecking>
<lastHandle> <lastHandle>
<entry key="editor">None</entry> <entry key="editor">None</entry>
+8 -6
View File
@@ -509,7 +509,6 @@ def testCoreTools_ProjectBuilderCopyPlain(monkeypatch, caplog, mockGUI, prjLipsu
data = { data = {
"name": "Test Project", "name": "Test Project",
"author": "Jane Doe", "author": "Jane Doe",
"language": "en_US",
"path": dstPath, "path": dstPath,
"template": srcPath, "template": srcPath,
} }
@@ -556,9 +555,9 @@ def testCoreTools_ProjectBuilderCopyPlain(monkeypatch, caplog, mockGUI, prjLipsu
assert srcProject.data.author == "lipsum.com" assert srcProject.data.author == "lipsum.com"
assert dstProject.data.author == "Jane Doe" assert dstProject.data.author == "Jane Doe"
# Language should be different # Language should be the same
assert srcProject.data.language == "en_GB" assert srcProject.data.language == "en_GB"
assert dstProject.data.language == "en_US" assert dstProject.data.language == "en_GB"
# Counts should be more or less zeroed # Counts should be more or less zeroed
assert dstProject.data.saveCount < 5 assert dstProject.data.saveCount < 5
@@ -634,9 +633,9 @@ def testCoreTools_ProjectBuilderCopyZipped(monkeypatch, caplog, mockGUI, fncPath
assert srcProject.data.author == "Jane Doe" assert srcProject.data.author == "Jane Doe"
assert dstProject.data.author == "Jane Doe" assert dstProject.data.author == "Jane Doe"
# Language should be different # Language should be the same
assert srcProject.data.language is None assert srcProject.data.language is None
assert dstProject.data.language == "en_US" assert dstProject.data.language is None
# Counts should be more or less zeroed # Counts should be more or less zeroed
assert dstProject.data.saveCount < 5 assert dstProject.data.saveCount < 5
@@ -652,7 +651,7 @@ def testCoreTools_ProjectBuilderSample(monkeypatch, mockGUI, fncPath, tstPaths):
data = { data = {
"name": "Test Sample", "name": "Test Sample",
"author": "Jane Doe", "author": "Jane Doe",
"path": fncPath, "path": fncPath / "project",
"sample": True, "sample": True,
} }
@@ -685,6 +684,9 @@ def testCoreTools_ProjectBuilderSample(monkeypatch, mockGUI, fncPath, tstPaths):
zipObj.write(docFile, f"content/{docFile.name}") zipObj.write(docFile, f"content/{docFile.name}")
assert builder.buildProject(data) is True assert builder.buildProject(data) is True
# Can't create to the same target again
assert builder.buildProject(data) is False
dstSample.unlink() dstSample.unlink()
# END Test testCoreTools_ProjectBuilderSample # END Test testCoreTools_ProjectBuilderSample
+8 -5
View File
@@ -25,18 +25,19 @@ import pytest
from shutil import copyfile from shutil import copyfile
from mocked import causeException
from novelwriter.core.item import NWItem
from tools import C, buildTestProject, cmpFiles, writeFile from tools import C, buildTestProject, cmpFiles, writeFile
from mocked import causeException
from novelwriter import SHARED
from novelwriter.enum import nwComment, nwItemClass, nwItemLayout from novelwriter.enum import nwComment, nwItemClass, nwItemLayout
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.item import NWItem
from novelwriter.core.index import IndexItem, NWIndex, countWords, TagsIndex, processComment from novelwriter.core.index import IndexItem, NWIndex, countWords, TagsIndex, processComment
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
@pytest.mark.core @pytest.mark.core
def testCoreIndex_LoadSave(monkeypatch, prjLipsum, mockGUI, tstPaths): def testCoreIndex_LoadSave(qtbot, monkeypatch, prjLipsum, mockGUI, tstPaths):
"""Test core functionality of scanning, saving, loading and checking """Test core functionality of scanning, saving, loading and checking
the index cache file. the index cache file.
""" """
@@ -148,8 +149,10 @@ def testCoreIndex_LoadSave(monkeypatch, prjLipsum, mockGUI, tstPaths):
assert "fb609cd8319dc" in index._itemIndex assert "fb609cd8319dc" in index._itemIndex
assert "7a992350f3eb6" in index._itemIndex assert "7a992350f3eb6" in index._itemIndex
# Finalise # Close Project
project.closeProject() with qtbot.waitSignal(SHARED.indexCleared, timeout=1000):
# Regression test for issue #1718
project.closeProject()
# END Test testCoreIndex_LoadSave # END Test testCoreIndex_LoadSave
+4 -1
View File
@@ -111,12 +111,15 @@ def testCoreStorage_InitProjectStorage(mockGUI, fncPath, mockRnd):
buildTestProject(project, fncPath) buildTestProject(project, fncPath)
# Init with the wrong file # Init with the wrong file
foo = fncPath / "foobar.txt"
foo.touch()
assert storage.initProjectStorage(fncPath / "foobar.txt") == NWStorageOpen.UNKOWN assert storage.initProjectStorage(fncPath / "foobar.txt") == NWStorageOpen.UNKOWN
foo.unlink()
storage._clearLockFile() storage._clearLockFile()
storage.clear() storage.clear()
# Init with the user's home dir # Init with the user's home dir
assert storage.initProjectStorage(Path.home()) == NWStorageOpen.UNKOWN assert storage.initProjectStorage(Path.home()) == NWStorageOpen.NOT_FOUND
storage._clearLockFile() storage._clearLockFile()
storage.clear() storage.clear()
+210 -10
View File
@@ -26,6 +26,7 @@ import pytest
from tools import C, buildTestProject, readFile from tools import C, buildTestProject, readFile
from novelwriter.constants import nwHeadFmt from novelwriter.constants import nwHeadFmt
from novelwriter.core.tomd import ToMarkdown
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import HeadingFormatter, Tokenizer, stripEscape from novelwriter.core.tokenizer import HeadingFormatter, Tokenizer, stripEscape
@@ -1010,7 +1011,7 @@ def testCoreToken_ProcessHeaders(mockGUI):
tokens._isNote = False tokens._isNote = False
## ##
# Story FIles # Story Files
## ##
tokens._isNone = False tokens._isNone = False
@@ -1122,7 +1123,7 @@ def testCoreToken_ProcessHeaders(mockGUI):
# H3: Scene wo/Format, first # H3: Scene wo/Format, first
tokens._text = "### Scene One\n" tokens._text = "### Scene One\n"
tokens.setSceneFormat("", False) tokens.setSceneFormat("", False)
tokens._firstScene = True tokens._skipSeparator = True
tokens.tokenizeText() tokens.tokenizeText()
tokens.doHeaders() tokens.doHeaders()
assert tokens._tokens == [ assert tokens._tokens == [
@@ -1133,7 +1134,7 @@ def testCoreToken_ProcessHeaders(mockGUI):
# H3: Scene wo/Format, not first # H3: Scene wo/Format, not first
tokens._text = "### Scene One\n" tokens._text = "### Scene One\n"
tokens.setSceneFormat("", False) tokens.setSceneFormat("", False)
tokens._firstScene = False tokens._skipSeparator = False
tokens.tokenizeText() tokens.tokenizeText()
tokens.doHeaders() tokens.doHeaders()
assert tokens._tokens == [ assert tokens._tokens == [
@@ -1144,7 +1145,7 @@ def testCoreToken_ProcessHeaders(mockGUI):
# H3: Scene Separator, first # H3: Scene Separator, first
tokens._text = "### Scene One\n" tokens._text = "### Scene One\n"
tokens.setSceneFormat("* * *", False) tokens.setSceneFormat("* * *", False)
tokens._firstScene = True tokens._skipSeparator = True
tokens.tokenizeText() tokens.tokenizeText()
tokens.doHeaders() tokens.doHeaders()
assert tokens._tokens == [ assert tokens._tokens == [
@@ -1155,7 +1156,7 @@ def testCoreToken_ProcessHeaders(mockGUI):
# H3: Scene Separator, not first # H3: Scene Separator, not first
tokens._text = "### Scene One\n" tokens._text = "### Scene One\n"
tokens.setSceneFormat("* * *", False) tokens.setSceneFormat("* * *", False)
tokens._firstScene = False tokens._skipSeparator = False
tokens.tokenizeText() tokens.tokenizeText()
tokens.doHeaders() tokens.doHeaders()
assert tokens._tokens == [ assert tokens._tokens == [
@@ -1192,7 +1193,7 @@ def testCoreToken_ProcessHeaders(mockGUI):
# H4: Section Hidden wo/Format # H4: Section Hidden wo/Format
tokens._text = "#### A Section\n" tokens._text = "#### A Section\n"
tokens.setSectionFormat(r"", True) tokens.setSectionFormat("", True)
tokens.tokenizeText() tokens.tokenizeText()
tokens.doHeaders() tokens.doHeaders()
assert tokens._tokens == [ assert tokens._tokens == [
@@ -1230,17 +1231,210 @@ def testCoreToken_ProcessHeaders(mockGUI):
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE), (Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
] ]
# Check the first scene detector # Check the first scene detector, plain text
assert tokens._firstScene is False tokens._skipSeparator = True
tokens._firstScene = True
tokens._text = "Some text ...\n" tokens._text = "Some text ...\n"
tokens.tokenizeText() tokens.tokenizeText()
tokens.doHeaders() tokens.doHeaders()
assert tokens._firstScene is False assert tokens._skipSeparator is True
# Check the first scene detector, text plus scene
tokens._skipSeparator = True
tokens._text = "Some text ...\n\n### Scene\n\nText"
tokens.tokenizeText()
tokens.doHeaders()
assert tokens._skipSeparator is False
# END Test testCoreToken_ProcessHeaders # END Test testCoreToken_ProcessHeaders
@pytest.mark.core
def testCoreToken_HeaderCounterAndVisibility(mockGUI):
"""Test the header counter and visibility of the Tokenizer class.
This is a special test to cover issue #1704.
"""
project = NWProject()
project.data.setLanguage("en")
project._loadProjectLocalisation()
md = ToMarkdown(project)
md._isNone = False
md._isNote = False
md._isNovel = True
# Separator Handling, Titles
# ==========================
md._text = (
"# Title One\n\n"
"### Scene One\n\n"
"Text\n\n"
"### Scene Two\n\n"
"Text\n\n"
"# Title Two\n\n"
"### Scene Three\n\n"
"Text\n\n"
"### Scene Four\n\n"
"Text\n\n"
)
md.setTitleFormat(f"T: {nwHeadFmt.TITLE}")
md.setChapterFormat(f"C: {nwHeadFmt.TITLE}")
md.setSectionFormat("", True)
# Static Separator
md.setSceneFormat("* * *", False)
md.tokenizeText()
md.doHeaders()
md.doConvert()
assert md.result == (
"# T: Title One\n\n"
"Text\n\n"
"* * *\n\n"
"Text\n\n"
"# T: Title Two\n\n"
"Text\n\n"
"* * *\n\n"
"Text\n\n"
)
# Scene Title Formatted
md.setSceneFormat(f"S: {nwHeadFmt.TITLE}", False)
md.tokenizeText()
md.doHeaders()
md.doConvert()
assert md.result == (
"# T: Title One\n\n"
"### S: Scene One\n\n"
"Text\n\n"
"### S: Scene Two\n\n"
"Text\n\n"
"# T: Title Two\n\n"
"### S: Scene Three\n\n"
"Text\n\n"
"### S: Scene Four\n\n"
"Text\n\n"
)
# Separator Handling, Chapters
# ============================
md._text = (
"# Title One\n\n"
"## Chapter One\n\n"
"### Scene One\n\n"
"Text\n\n"
"### Scene Two\n\n"
"Text\n\n"
"## Chapter Two\n\n"
"### Scene Three\n\n"
"Text\n\n"
"### Scene Four\n\n"
"Text\n\n"
)
md.setTitleFormat(f"T: {nwHeadFmt.TITLE}")
md.setChapterFormat(f"C: {nwHeadFmt.TITLE}")
md.setSectionFormat("", True)
# Static Separator
md.setSceneFormat("* * *", False)
md.tokenizeText()
md.doHeaders()
md.doConvert()
assert md.result == (
"# T: Title One\n\n"
"## C: Chapter One\n\n"
"Text\n\n"
"* * *\n\n"
"Text\n\n"
"## C: Chapter Two\n\n"
"Text\n\n"
"* * *\n\n"
"Text\n\n"
)
# Scene Title Formatted
md.setSceneFormat(f"S: {nwHeadFmt.TITLE}", False)
md.tokenizeText()
md.doHeaders()
md.doConvert()
assert md.result == (
"# T: Title One\n\n"
"## C: Chapter One\n\n"
"### S: Scene One\n\n"
"Text\n\n"
"### S: Scene Two\n\n"
"Text\n\n"
"## C: Chapter Two\n\n"
"### S: Scene Three\n\n"
"Text\n\n"
"### S: Scene Four\n\n"
"Text\n\n"
)
# Counter Handling, Novel Titles
# ==============================
md._text = (
"#! Novel One\n\n"
"## Chapter One\n\n"
"### Scene One\n\n"
"Text\n\n"
"### Scene Two\n\n"
"Text\n\n"
"## Chapter Two\n\n"
"### Scene Three\n\n"
"Text\n\n"
"### Scene Four\n\n"
"Text\n\n"
"#! Novel Two\n\n"
"## Chapter One\n\n"
"### Scene One\n\n"
"Text\n\n"
"### Scene Two\n\n"
"Text\n\n"
"## Chapter Two\n\n"
"### Scene Three\n\n"
"Text\n\n"
"### Scene Four\n\n"
"Text\n\n"
)
md.setTitleFormat(f"T: {nwHeadFmt.TITLE}")
md.setChapterFormat(f"C {nwHeadFmt.CH_NUM}: {nwHeadFmt.TITLE}")
md.setSceneFormat(f"S {nwHeadFmt.CH_NUM}.{nwHeadFmt.SC_NUM} ({nwHeadFmt.SC_ABS}): "
f"{nwHeadFmt.TITLE}", False)
md.setSectionFormat("", True)
# Two Novel Format
md.tokenizeText()
md.doHeaders()
md.doConvert()
assert md.result == (
"# Novel One\n\n"
"## C 1: Chapter One\n\n"
"### S 1.1 (1): Scene One\n\n"
"Text\n\n"
"### S 1.2 (2): Scene Two\n\n"
"Text\n\n"
"## C 2: Chapter Two\n\n"
"### S 2.1 (3): Scene Three\n\n"
"Text\n\n"
"### S 2.2 (4): Scene Four\n\n"
"Text\n\n"
"# Novel Two\n\n"
"## C 1: Chapter One\n\n"
"### S 1.1 (1): Scene One\n\n"
"Text\n\n"
"### S 1.2 (2): Scene Two\n\n"
"Text\n\n"
"## C 2: Chapter Two\n\n"
"### S 2.1 (3): Scene Three\n\n"
"Text\n\n"
"### S 2.2 (4): Scene Four\n\n"
"Text\n\n"
)
# END Test testCoreToken_HeaderCounterAndVisibility
@pytest.mark.core @pytest.mark.core
def testCoreIndex_HeadingFormatter(fncPath, mockRnd): def testCoreIndex_HeadingFormatter(fncPath, mockRnd):
"""Check the HeadingFormatter class.""" """Check the HeadingFormatter class."""
@@ -1306,6 +1500,12 @@ def testCoreIndex_HeadingFormatter(fncPath, mockRnd):
formatter.incScene() formatter.incScene()
assert formatter.apply(cFormat, "Hi Bob", 1) == "Chapter 2.1 - Scene 5 - Hi Bob" assert formatter.apply(cFormat, "Hi Bob", 1) == "Chapter 2.1 - Scene 5 - Hi Bob"
# New Main Title
formatter.resetAll()
formatter.incChapter()
formatter.incScene()
assert formatter.apply(cFormat, "Hi Bob", 1) == "Chapter 1.1 - Scene 1 - Hi Bob"
# Special Formats # Special Formats
# =============== # ===============
formatter._chCount = 2 formatter._chCount = 2
+41 -8
View File
@@ -23,30 +23,63 @@ from __future__ import annotations
import sys import sys
import pytest import pytest
from PyQt5.QtWidgets import qApp, QMessageBox from collections.abc import Callable
from novelwriter import CONFIG, main from tools import buildTestProject
from PyQt5.QtWidgets import QDialog, qApp, QMessageBox
from novelwriter import CONFIG, SHARED
from novelwriter.dialogs.about import GuiAbout
from novelwriter.dialogs.wordlist import GuiWordList
from novelwriter.dialogs.preferences import GuiPreferences
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
LANG_DATA = CONFIG.listLanguages(CONFIG.LANG_NW) LANG_DATA = CONFIG.listLanguages(CONFIG.LANG_NW)
@pytest.mark.gui @pytest.mark.gui
@pytest.mark.skipif(not sys.platform.startswith("linux"), reason="Linux Only") @pytest.mark.skipif(sys.platform != "linux", reason="Linux Only")
@pytest.mark.skipif(not LANG_DATA, reason="No i18n Data") @pytest.mark.skipif(not LANG_DATA, reason="No i18n Data")
@pytest.mark.parametrize("language", [a for a, b in LANG_DATA]) @pytest.mark.parametrize("language", [a for a, b in LANG_DATA])
def testI18n_Localisation(qtbot, monkeypatch, language, fncPath): def testGuiI18n_Localisation(qtbot, monkeypatch, language, nwGUI, projPath):
"""Test loading the gui with a specific language.""" """Test loading the gui with a specific language."""
monkeypatch.setattr(QDialog, "exec_", lambda *a: None)
monkeypatch.setattr(QMessageBox, "exec_", lambda *a: None) monkeypatch.setattr(QMessageBox, "exec_", lambda *a: None)
monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.Yes)
# Set the test langauge # Set the test language
CONFIG.guiLocale = language CONFIG.guiLocale = language
CONFIG.initLocalisation(qApp) CONFIG.initLocalisation(qApp)
nwGUI = main(["--testmode", f"--config={fncPath}", f"--data={fncPath}"]) buildTestProject(nwGUI, projPath)
qtbot.addWidget(nwGUI)
nwGUI.show() nwGUI.show()
def showDialog(func: Callable, dType: QDialog) -> None:
func()
qtbot.waitUntil(lambda: SHARED.findTopLevelWidget(dType) is not None, timeout=1000)
dialog = SHARED.findTopLevelWidget(dType)
assert isinstance(dialog, dType)
with qtbot.waitExposed(dialog):
dialog.show()
dialog.close()
showDialog(nwGUI.showWelcomeDialog, GuiWelcome)
showDialog(nwGUI.showPreferencesDialog, GuiPreferences)
showDialog(nwGUI.showProjectSettingsDialog, GuiProjectSettings)
showDialog(nwGUI.showNovelDetailsDialog, GuiNovelDetails)
showDialog(nwGUI.showBuildManuscriptDialog, GuiManuscript)
showDialog(nwGUI.showProjectWordListDialog, GuiWordList)
showDialog(nwGUI.showWritingStatsDialog, GuiWritingStats)
showDialog(nwGUI.showAboutNWDialog, GuiAbout)
showDialog(nwGUI.showDictionariesDialog, GuiDictionaries)
qtbot.wait(20) qtbot.wait(20)
nwGUI.closeMain() nwGUI.closeMain()
# END Test testI18n_Localisation # END Test testGuiI18n_Localisation
+16
View File
@@ -1404,6 +1404,22 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
assert SHARED.project.tree[hNovelNote].itemRoot == hTrashRoot # type: ignore assert SHARED.project.tree[hNovelNote].itemRoot == hTrashRoot # type: ignore
assert SHARED.project.tree[hSubNote].itemRoot == hTrashRoot # type: ignore assert SHARED.project.tree[hSubNote].itemRoot == hTrashRoot # type: ignore
# Permanently Delete Menu
nwItem = SHARED.project.tree[hCharNote]
assert isinstance(nwItem, NWItem)
ctxMenu = _TreeContextMenu(projTree, nwItem)
ctxMenu.buildMultiSelectMenu([hCharNote, hNovelNote, hSubNote])
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Set Active to ...", "Set Importance to ...", "Delete Permanently",
]
# Permanently Delete
ctxMenu._iterPermDelete()
assert SHARED.project.tree[hCharNote] is None
assert SHARED.project.tree[hNovelNote] is None
assert SHARED.project.tree[hSubNote] is None
# qtbot.stop() # qtbot.stop()
# END Test testGuiProjTree_ContextMenu # END Test testGuiProjTree_ContextMenu
+3 -2
View File
@@ -141,11 +141,12 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI):
mp.setattr("builtins.open", causeOSError) mp.setattr("builtins.open", causeOSError)
assert mainTheme.listThemes() == [] assert mainTheme.listThemes() == []
# Load the theme info # Load the theme info, default themes first
themesList = mainTheme.listThemes() themesList = mainTheme.listThemes()
assert themesList[0] == ("default_dark", "Default Dark Theme") assert themesList[0] == ("default_dark", "Default Dark Theme")
assert themesList[1] == ("default_light", "Default Light Theme") assert themesList[1] == ("default_light", "Default Light Theme")
assert themesList[2] == ("default", "Qt Default Theme") assert themesList[2] == ("cyberpunk_night", "Cyberpunk Night")
assert themesList[3] == ("default", "Qt Default Theme")
# A second call should returned the cached list # A second call should returned the cached list
assert mainTheme.listThemes() == mainTheme._themeList assert mainTheme.listThemes() == mainTheme._themeList
+1 -1
View File
@@ -47,7 +47,7 @@ def testManuscript_Init(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath: Pat
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath) nwGUI.openProject(projPath)
SHARED.project.storage.getDocument(C.hChapterDoc).writeDocument("## A Chapter\n\n\t\tHi") SHARED.project.storage.getDocument(C.hChapterDoc).writeDocument("## A Chapter\n\n\t\tHi")
allText = "New Novel\nBy Jane Doe\nA Chapter\n\t\tHi\n* * *" allText = "New Novel\nBy Jane Doe\nA Chapter\n\t\tHi"
nwGUI.mainMenu.aBuildManuscript.activate(QAction.Trigger) nwGUI.mainMenu.aBuildManuscript.activate(QAction.Trigger)
qtbot.waitUntil(lambda: SHARED.findTopLevelWidget(GuiManuscript) is not None, timeout=1000) qtbot.waitUntil(lambda: SHARED.findTopLevelWidget(GuiManuscript) is not None, timeout=1000)
+18 -26
View File
@@ -47,18 +47,18 @@ def testToolWelcome_Main(qtbot: QtBot, monkeypatch, nwGUI, fncPath):
assert welcome.mainStack.currentIndex() == 0 assert welcome.mainStack.currentIndex() == 0
# Show the new project form # Show the new project form
welcome.newButton.click() welcome.btnNew.click()
assert welcome.mainStack.currentIndex() == 1 assert welcome.mainStack.currentIndex() == 1
# Revert to project lits # Revert to project lits
welcome.tabNew.cancelNewProject.emit() welcome.btnList.click()
assert welcome.mainStack.currentIndex() == 0 assert welcome.mainStack.currentIndex() == 0
# Open a project # Open a project
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(SHARED, "getProjectPath", lambda *a, **k: fncPath) mp.setattr(SHARED, "getProjectPath", lambda *a, **k: fncPath)
with qtbot.waitSignal(welcome.openProjectRequest) as signal: with qtbot.waitSignal(welcome.openProjectRequest) as signal:
welcome.browseButton.click() welcome.btnBrowse.click()
assert signal.args and signal.args[0] == fncPath assert signal.args and signal.args[0] == fncPath
# qtbot.stop() # qtbot.stop()
@@ -103,7 +103,7 @@ def testToolWelcome_Open(qtbot: QtBot, monkeypatch, nwGUI, fncPath):
qtbot.mouseClick(vPort, Qt.MouseButton.LeftButton, pos=posTwo, delay=10) qtbot.mouseClick(vPort, Qt.MouseButton.LeftButton, pos=posTwo, delay=10)
assert tabOpen.selectedPath.text() == "Path: /stuff/project_one" assert tabOpen.selectedPath.text() == "Path: /stuff/project_one"
# Double Click item # Double click item
qtbot.mouseClick(vPort, Qt.MouseButton.LeftButton, pos=posTwo, delay=10) qtbot.mouseClick(vPort, Qt.MouseButton.LeftButton, pos=posTwo, delay=10)
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(welcome, "close", lambda *a: None) mp.setattr(welcome, "close", lambda *a: None)
@@ -111,6 +111,14 @@ def testToolWelcome_Open(qtbot: QtBot, monkeypatch, nwGUI, fncPath):
qtbot.mouseDClick(vPort, Qt.MouseButton.LeftButton, pos=posTwo, delay=10) qtbot.mouseDClick(vPort, Qt.MouseButton.LeftButton, pos=posTwo, delay=10)
assert signal.args and signal.args[0] == Path("/stuff/project_one") assert signal.args and signal.args[0] == Path("/stuff/project_one")
# Press open button
qtbot.mouseClick(vPort, Qt.MouseButton.LeftButton, pos=posTwo, delay=10)
with monkeypatch.context() as mp:
mp.setattr(welcome, "close", lambda *a: None)
with qtbot.waitSignal(welcome.openProjectRequest, timeout=5000) as signal:
welcome.btnOpen.click()
assert signal.args and signal.args[0] == Path("/stuff/project_one")
# Context Menu # Context Menu
def getMenuForPos(pos: QPoint) -> QMenu | None: def getMenuForPos(pos: QPoint) -> QMenu | None:
nonlocal tabOpen nonlocal tabOpen
@@ -165,7 +173,7 @@ def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath):
with qtbot.waitExposed(welcome): with qtbot.waitExposed(welcome):
welcome.show() welcome.show()
welcome.newButton.click() welcome.btnNew.click()
assert welcome.mainStack.currentIndex() == 1 assert welcome.mainStack.currentIndex() == 1
tabNew = welcome.tabNew tabNew = welcome.tabNew
newForm = tabNew.projectForm newForm = tabNew.projectForm
@@ -188,12 +196,7 @@ def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath):
newForm.fillSample.trigger() newForm.fillSample.trigger()
assert newForm._fillMode == newForm.FILL_SAMPLE assert newForm._fillMode == newForm.FILL_SAMPLE
assert newForm.projFill.text() == "Example Project" assert newForm.projFill.text() == "Example Project"
assert newForm.addNotes.isEnabled() is False assert newForm.extraWidget.isVisible() is False
assert newForm.addPlot.isEnabled() is False
assert newForm.addChar.isEnabled() is False
assert newForm.addWorld.isEnabled() is False
assert newForm.numChapters.isEnabled() is False
assert newForm.numScenes.isEnabled() is False
# Change fill info to template # Change fill info to template
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
@@ -201,12 +204,7 @@ def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath):
newForm.fillCopy.trigger() newForm.fillCopy.trigger()
assert newForm._fillMode == newForm.FILL_COPY assert newForm._fillMode == newForm.FILL_COPY
assert newForm.projFill.text() == f"Template: {fncPath}" assert newForm.projFill.text() == f"Template: {fncPath}"
assert newForm.addNotes.isEnabled() is False assert newForm.extraWidget.isVisible() is False
assert newForm.addPlot.isEnabled() is False
assert newForm.addChar.isEnabled() is False
assert newForm.addWorld.isEnabled() is False
assert newForm.numChapters.isEnabled() is False
assert newForm.numScenes.isEnabled() is False
# Change back to fill blank using the menu # Change back to fill blank using the menu
newForm.browseFill.click() newForm.browseFill.click()
@@ -215,16 +213,11 @@ def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath):
newForm.fillMenu.close() newForm.fillMenu.close()
assert newForm._fillMode == newForm.FILL_BLANK assert newForm._fillMode == newForm.FILL_BLANK
assert newForm.projFill.text() == "Fresh Project" assert newForm.projFill.text() == "Fresh Project"
assert newForm.addNotes.isEnabled() is True assert newForm.extraWidget.isVisible() is True
assert newForm.addPlot.isEnabled() is True
assert newForm.addChar.isEnabled() is True
assert newForm.addWorld.isEnabled() is True
assert newForm.numChapters.isEnabled() is True
assert newForm.numScenes.isEnabled() is True
# Creating a project without a name, pops an error # Creating a project without a name, pops an error
caplog.clear() caplog.clear()
tabNew.createButton.click() welcome.btnCreate.click()
assert "A project name is required." in caplog.text assert "A project name is required." in caplog.text
# Set some more values, and extract data # Set some more values, and extract data
@@ -240,7 +233,6 @@ def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath):
assert newForm.getProjectData() == { assert newForm.getProjectData() == {
"name": "Test Project", "name": "Test Project",
"author": "Jane Smith", "author": "Jane Smith",
"language": "en_GB",
"path": str(projPath), "path": str(projPath),
"blank": True, "blank": True,
"sample": False, "sample": False,
@@ -253,7 +245,7 @@ def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath):
# Create a project with these values # Create a project with these values
with qtbot.waitSignal(welcome.openProjectRequest, timeout=5000) as signal: with qtbot.waitSignal(welcome.openProjectRequest, timeout=5000) as signal:
tabNew.createButton.click() welcome.btnCreate.click()
assert signal.args and signal.args[0] == projPath assert signal.args and signal.args[0] == projPath
assert (projPath / nwFiles.PROJ_FILE).exists() assert (projPath / nwFiles.PROJ_FILE).exists()