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
## 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]
### 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"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__version__ = "2.3b1"
__hexversion__ = "0x020300b1"
__date__ = "2024-02-16"
__version__ = "2.3rc1"
__hexversion__ = "0x020300c1"
__date__ = "2024-02-24"
__status__ = "Stable"
__domain__ = "novelwriter.io"
@@ -1,7 +1,18 @@
{
"Synopsis": "Synopsis",
"Short Description": "Korte beschrijving",
"Comment": "Opmerking",
"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",
"1": "één",
"2": "twee",
@@ -1,7 +1,18 @@
{
"Synopsis": "Sinopse",
"Short Description": "Descrição breve",
"Comment": "Comentários",
"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",
"1": "Um",
"2": "Dois",
@@ -40,7 +40,7 @@ cls_none = typ_cancel.svg
cls_novel = typ_book.svg
cls_object = typ_key.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_trash = typ_trash.svg
cls_world = typ_location.svg
@@ -60,11 +60,13 @@ fmt_superscript = nw_tb-superscript.svg
fmt_underline = nw_tb-underline.svg
forward = typ_chevron-right.svg
import = mixed_import.svg
list = typ_th-list.svg
maximise = typ_arrow-maximise.svg
menu = typ_th-dot-menu.svg
minimise = typ_arrow-minimise.svg
more = typ_th-dot-more.svg
noncheckable = mixed_input-none.svg
open = typ_folder.svg
panel = nw_panel.svg
proj_chapter = mixed_document-chapter.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_object = typ_key.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_trash = typ_trash.svg
cls_world = typ_location.svg
@@ -60,11 +60,13 @@ fmt_superscript = nw_tb-superscript.svg
fmt_underline = nw_tb-underline.svg
forward = typ_chevron-right.svg
import = mixed_import.svg
list = typ_th-list.svg
maximise = typ_arrow-maximise.svg
menu = typ_th-dot-menu.svg
minimise = typ_arrow-minimise.svg
more = typ_th-dot-more.svg
noncheckable = mixed_input-none.svg
open = typ_folder.svg
panel = nw_panel.svg
proj_chapter = mixed_document-chapter.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")
# Settings
projName = data.get("name", lblNewProject)
projAuthor = data.get("author", "")
projLang = data.get("language", "en_GB")
project.data.setUuid(None)
project.data.setName(projName)
project.data.setAuthor(projAuthor)
project.data.setLanguage(projLang)
project.data.setName(data.get("name", lblNewProject))
project.data.setAuthor(data.get("author", ""))
project.data.setLanguage(CONFIG.guiLocale)
project.setDefaultStatusImport()
project.session.startSession()
@@ -502,7 +498,6 @@ class ProjectBuilder:
project.data.setUuid("") # Creates a fresh uuid
project.data.setName(data.get("name", "None"))
project.data.setAuthor(data.get("author", ""))
project.data.setLanguage(data.get("language", "en_GB"))
project.data.setSpellCheck(True)
project.data.setSpellLang(None)
project.data.setDoBackup(True)
@@ -518,6 +513,13 @@ class ProjectBuilder:
"""Make a copy of the sample project by extracting the
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():
try:
shutil.unpack_archive(sample, path)
+1
View File
@@ -410,6 +410,7 @@ class NWProject:
def closeProject(self, idleTime: float = 0.0) -> None:
"""Close the project."""
logger.info("Closing project")
self._index.clearIndex() # Triggers clear signal, see #1718
self._options.saveSettings()
self._tree.writeToCFile()
self._session.appendSession(idleTime)
+8 -4
View File
@@ -183,11 +183,15 @@ class NWStorage:
# 2. A full path to an nwProject.nwx file
if inPath.is_dir() and inPath != Path.home().resolve():
nwxFile = inPath / nwFiles.PROJ_FILE
elif inPath.is_file() and inPath.name == nwFiles.PROJ_FILE:
nwxFile = inPath
elif inPath.is_file():
if inPath.name == nwFiles.PROJ_FILE:
nwxFile = inPath
else:
logger.error("Not a novelWriter project")
return NWStorageOpen.UNKOWN
else:
logger.error("Not a novelWriter project")
return NWStorageOpen.UNKOWN
logger.error("Not found: %s", inPath)
return NWStorageOpen.NOT_FOUND
if not nwxFile.exists():
# The .nwx file must exist to continue
+62 -63
View File
@@ -89,7 +89,7 @@ class Tokenizer(ABC):
T_UNNUM = 7 # Unnumbered
T_HEAD1 = 8 # Header 1
T_HEAD2 = 9 # Header 2
T_HEAD3 = 10 # Header 3
T_HEAD3 = 10 # Header 3
T_HEAD4 = 11 # Header 4
T_TEXT = 12 # Text line
T_SEP = 13 # Scene separator
@@ -158,7 +158,7 @@ class Tokenizer(ABC):
# Instance Variables
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
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
# on the very first page, adding a blank first page.
if self._tokens[0][4] & self.A_PBB:
tToken = self._tokens[0]
token = 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
@@ -637,22 +637,20 @@ class Tokenizer(ABC):
pToken = (self.T_EMPTY, 0, "", [], self.A_NONE)
nToken = (self.T_EMPTY, 0, "", [], self.A_NONE)
tCount = len(self._tokens)
for n, tToken in enumerate(self._tokens):
for n, token in enumerate(self._tokens):
if n > 0:
pToken = self._tokens[n-1]
if n < tCount - 1:
nToken = self._tokens[n+1]
if tToken[0] == self.T_KEYWORD:
aStyle = tToken[4]
if token[0] == self.T_KEYWORD:
aStyle = token[4]
if pToken[0] == self.T_KEYWORD:
aStyle |= self.A_Z_TOPMRG
if nToken[0] == self.T_KEYWORD:
aStyle |= self.A_Z_BTMMRG
self._tokens[n] = (
tToken[0], tToken[1], tToken[2], tToken[3], aStyle
)
self._tokens[n] = (token[0], token[1], token[2], token[3], aStyle)
return
@@ -665,93 +663,87 @@ class Tokenizer(ABC):
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 tToken[0] == self.T_TEXT:
self._firstScene = False
if token[0] == self.T_TITLE: # Title
# For new titles, we reset all counters
self._skipSeparator = True
self._hFormatter.resetAll()
elif tToken[0] == self.T_HEAD1:
# Partition
elif token[0] == self.T_HEAD1: # Partition
tTemp = self._hFormatter.apply(self._fmtTitle, tToken[2], tToken[1])
tTemp = self._hFormatter.apply(self._fmtTitle, token[2], token[1])
self._tokens[n] = (
tToken[0], tToken[1], tTemp, [], tToken[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]
token[0], token[1], tTemp, [], token[4]
)
# Set scene variables
self._firstScene = True
self._skipSeparator = True
self._hFormatter.resetScene()
elif tToken[0] == self.T_HEAD3:
# Scene
elif token[0] in (self.T_HEAD2, self.T_UNNUM): # Chapter, Unnumbered
# 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()
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:
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:
if self._firstScene:
self._tokens[n] = (
self.T_EMPTY, tToken[1], "", [], self.A_NONE
)
else:
self._tokens[n] = (
self.T_SKIP, tToken[1], "", [], tToken[4]
)
self._tokens[n] = (
self.T_EMPTY if self._skipSeparator else self.T_SKIP, token[1],
"", [], self.A_NONE if self._skipSeparator else token[4]
)
elif tTemp == self._fmtScene:
if self._firstScene:
self._tokens[n] = (
self.T_EMPTY, tToken[1], "", [], self.A_NONE
)
else:
self._tokens[n] = (
self.T_SEP, tToken[1], tTemp, [], tToken[4] | self.A_CENTRE
)
self._tokens[n] = (
self.T_EMPTY if self._skipSeparator else self.T_SEP, token[1],
"" if self._skipSeparator else tTemp, [],
self.A_NONE if self._skipSeparator else (token[4] | self.A_CENTRE)
)
else:
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:
# Section
elif token[0] == self.T_HEAD4: # 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:
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:
self._tokens[n] = (
self.T_SKIP, tToken[1], "", [], tToken[4]
self.T_SKIP, token[1], "", [], token[4]
)
elif tTemp == self._fmtSection:
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:
self._tokens[n] = (
tToken[0], tToken[1], tTemp, [], tToken[4]
token[0], token[1], tTemp, [], token[4]
)
return True
@@ -849,6 +841,13 @@ class HeadingFormatter:
self._scAbsCount += 1
return
def resetAll(self) -> None:
"""Reset all counters."""
self._chCount = 0
self._scChCount = 0
self._scAbsCount = 0
return
def resetScene(self) -> None:
"""Reset the chapter scene counter."""
self._scChCount = 0
+2 -2
View File
@@ -68,9 +68,9 @@ class GuiAbout(QDialog):
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>"
)))
))
self.nwLicence.setOpenExternalLinks(True)
# Credits
+53 -45
View File
@@ -505,10 +505,15 @@ class GuiProjectTree(QTreeWidget):
self.mainGui = projView.mainGui
# Internal Variables
self._treeMap = {}
self._treeMap: dict[str, QTreeWidgetItem] = {}
self._timeChanged = 0.0
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
# =========
@@ -555,10 +560,6 @@ class GuiProjectTree(QTreeWidget):
trRoot = self.invisibleRootItem()
trRoot.setFlags(trRoot.flags() ^ Qt.ItemFlag.ItemIsDropEnabled)
# Cached values
self._lblActive = self.tr("Active")
self._lblInactive = self.tr("Inactive")
# Set selection options
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
@@ -902,10 +903,7 @@ class GuiProjectTree(QTreeWidget):
SHARED.info(self.tr("The Trash folder is already empty."))
return False
msgYes = SHARED.question(
self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash)
)
if not msgYes:
if not SHARED.question(self.trPermDelete.format(nTrash)):
logger.info("Action cancelled by user")
return False
@@ -1052,7 +1050,7 @@ class GuiProjectTree(QTreeWidget):
if nwItem.isFileType():
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)
else:
iconName = "noncheckable"
@@ -1129,9 +1127,8 @@ class GuiProjectTree(QTreeWidget):
"""Get the currently selected handle. If multiple items are
selected, return the first.
"""
selItem = self.selectedItems()
if selItem:
return selItem[0].data(self.C_DATA, self.D_HANDLE)
if items := self.selectedItems():
return items[0].data(self.C_DATA, self.D_HANDLE)
return None
def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> bool:
@@ -1143,9 +1140,8 @@ class GuiProjectTree(QTreeWidget):
if tHandle in self._treeMap:
self.setCurrentItem(self._treeMap[tHandle])
selIndex = self.selectedIndexes()
if selIndex and doScroll:
self.scrollTo(selIndex[0], QAbstractItemView.ScrollHint.PositionAtCenter)
if (indexes := self.selectedIndexes()) and doScroll:
self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter)
return True
@@ -1160,10 +1156,8 @@ class GuiProjectTree(QTreeWidget):
def openContextOnSelected(self) -> bool:
"""Open the context menu on the current selected item."""
selItem = self.selectedItems()
if selItem:
pos = self.visualItemRect(selItem[0]).center()
return self._openContextMenu(pos)
if items := self.selectedItems():
return self._openContextMenu(self.visualItemRect(items[0]).center())
return False
def changedSince(self, checkTime: float) -> bool:
@@ -1746,7 +1740,7 @@ class _TreeContextMenu(QMenu):
self._item = nwItem
self._handle = nwItem.itemHandle
self._items: list[str] = []
self._items: list[NWItem] = []
logger.debug("Ready: _TreeContextMenu")
@@ -1799,13 +1793,17 @@ class _TreeContextMenu(QMenu):
return
def buildMultiSelectMenu(self, items: list[str]) -> None:
def buildMultiSelectMenu(self, handles: list[str]) -> None:
"""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._itemStatusImport(True)
self.addSeparator()
self._moveToTrash(True)
self._multiMoveToTrash()
return
##
@@ -1847,9 +1845,9 @@ class _TreeContextMenu(QMenu):
"""Add Active/Inactive actions."""
if multi:
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))
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))
else:
action = self.addAction(self.tr("Toggle Active"))
@@ -1964,10 +1962,14 @@ class _TreeContextMenu(QMenu):
return
def _moveToTrash(self, multi: bool) -> None:
def _multiMoveToTrash(self) -> None:
"""Add move to Trash action."""
action = self.addAction(self.tr("Move to Trash"))
if multi:
areTrash = [i.itemClass == nwItemClass.TRASH for i in self._items]
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)
return
@@ -1979,10 +1981,19 @@ class _TreeContextMenu(QMenu):
def _iterMoveToTrash(self) -> None:
"""Iterate through files and move them to Trash."""
if SHARED.question(self.tr("Move {0} items to Trash?").format(len(self._items))):
for tHandle in self._items:
tItem = SHARED.project.tree[tHandle]
if tItem and tItem.isFileType():
self.projTree.moveItemToTrash(tHandle, askFirst=False, flush=False)
for tItem in self._items:
if tItem.isFileType() and tItem.itemClass != nwItemClass.TRASH:
self.projTree.moveItemToTrash(tItem.itemHandle, 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()
return
@@ -2000,12 +2011,11 @@ class _TreeContextMenu(QMenu):
def _iterItemActive(self, isActive: bool) -> None:
"""Set the active status of multiple items."""
for tHandle in self._items:
tItem = SHARED.project.tree[tHandle]
for tItem in self._items:
if tItem and tItem.isFileType():
tItem.setActive(isActive)
self.projTree.setTreeItemValues(tHandle)
self.projTree._alertTreeChange(tHandle, flush=False)
self.projTree.setTreeItemValues(tItem.itemHandle)
self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
return
def _changeItemStatus(self, key: str) -> None:
@@ -2017,12 +2027,11 @@ class _TreeContextMenu(QMenu):
def _iterSetItemStatus(self, key: str) -> None:
"""Change the status value for multiple items."""
for tHandle in self._items:
tItem = SHARED.project.tree[tHandle]
for tItem in self._items:
if tItem and tItem.isNovelLike():
tItem.setStatus(key)
self.projTree.setTreeItemValues(tHandle)
self.projTree._alertTreeChange(tHandle, flush=False)
self.projTree.setTreeItemValues(tItem.itemHandle)
self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
return
def _changeItemImport(self, key: str) -> None:
@@ -2034,12 +2043,11 @@ class _TreeContextMenu(QMenu):
def _iterSetItemImport(self, key: str) -> None:
"""Change the status value for multiple items."""
for tHandle in self._items:
tItem = SHARED.project.tree[tHandle]
for tItem in self._items:
if tItem and not tItem.isNovelLike():
tItem.setImport(key)
self.projTree.setTreeItemValues(tHandle)
self.projTree._alertTreeChange(tHandle, flush=False)
self.projTree.setTreeItemValues(tItem.itemHandle)
self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
return
def _changeItemLayout(self, itemLayout: nwItemLayout) -> None:
+12 -5
View File
@@ -344,7 +344,7 @@ class GuiTheme:
if 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
@@ -360,7 +360,7 @@ class GuiTheme:
if 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
@@ -453,9 +453,9 @@ class GuiIcons:
# General Button Icons
"add", "add_document", "backward", "bookmark", "browse", "checked", "close", "cross",
"document", "down", "edit", "export", "forward", "import", "maximise", "menu", "minimise",
"more", "noncheckable", "panel", "refresh", "remove", "revert", "search_replace", "search",
"settings", "star", "unchecked", "up", "view",
"document", "down", "edit", "export", "forward", "import", "list", "maximise", "menu",
"minimise", "more", "noncheckable", "open", "panel", "refresh", "remove", "revert",
"search_replace", "search", "settings", "star", "unchecked", "up", "view",
# Switches
"sticky-on", "sticky-off",
@@ -725,6 +725,13 @@ class GuiIcons:
# 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:
"""Open a conf file and read the 'name' setting."""
try:
+5 -3
View File
@@ -796,7 +796,7 @@ class GuiMain(QMainWindow):
def showWelcomeDialog(self) -> None:
"""Open the welcome dialog."""
dialog = GuiWelcome(self)
dialog.openProjectRequest.connect(self._openProject)
dialog.openProjectRequest.connect(self._openProjectFromWelcome)
dialog.exec_()
return
@@ -1132,10 +1132,12 @@ class GuiMain(QMainWindow):
return
@pyqtSlot(Path)
def _openProject(self, path: Path) -> None:
"""Handle an open project request."""
def _openProjectFromWelcome(self, path: Path) -> None:
"""Handle an open project request from the welcome dialog."""
qApp.processEvents()
self.openProject(path)
if not SHARED.hasProject:
self.showWelcomeDialog()
return
@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("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 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.setHorizontalSpacing(hPx)
self.projForm.setVerticalSpacing(vPx)
+78 -61
View File
@@ -34,10 +34,10 @@ from PyQt5.QtCore import (
pyqtSignal, pyqtSlot
)
from PyQt5.QtWidgets import (
QAction, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QHBoxLayout,
QLabel, QLineEdit, QListView, QMenu, QPushButton, QScrollArea, QShortcut,
QStackedWidget, QStyle, QStyleOptionViewItem, QStyledItemDelegate,
QToolButton, QVBoxLayout, QWidget, qApp
QAction, QDialog, QFileDialog, QFormLayout, QHBoxLayout, QLabel, QLineEdit,
QListView, QMenu, QPushButton, QScrollArea, QShortcut, QStackedWidget,
QStyle, QStyleOptionViewItem, QStyledItemDelegate, QToolButton,
QVBoxLayout, QWidget, qApp
)
from novelwriter import CONFIG, SHARED
@@ -46,7 +46,7 @@ from novelwriter.common import formatInt, makeFileNameSafe
from novelwriter.constants import nwFiles
from novelwriter.core.coretools import ProjectBuilder
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
logger = logging.getLogger(__name__)
@@ -97,7 +97,6 @@ class GuiWelcome(QDialog):
self.tabOpen.openProjectRequest.connect(self._openProjectPath)
self.tabNew = _NewProjectPage(self)
self.tabNew.cancelNewProject.connect(self._showOpenProjectPage)
self.tabNew.openProjectRequest.connect(self._openProjectPath)
self.mainStack = QStackedWidget(self)
@@ -107,17 +106,39 @@ class GuiWelcome(QDialog):
# Buttons
# =======
self.btnBox = QDialogButtonBox(QDialogButtonBox.Open | QDialogButtonBox.Cancel, self)
self.btnBox.accepted.connect(self.tabOpen.openSelectedItem)
self.btnBox.rejected.connect(self.close)
self.btnList = QPushButton(self.tr("List"), self)
self.btnList.setIcon(SHARED.theme.getIcon("list"))
self.btnList.clicked.connect(self._showOpenProjectPage)
self.newButton = self.btnBox.addButton(self.tr("New Project"), QDialogButtonBox.ActionRole)
self.newButton.setIcon(SHARED.theme.getIcon("add"))
self.newButton.clicked.connect(self._showNewProjectPage)
self.btnNew = QPushButton(self.tr("New"), self)
self.btnNew.setIcon(SHARED.theme.getIcon("add"))
self.btnNew.clicked.connect(self._showNewProjectPage)
self.browseButton = self.btnBox.addButton(self.tr("Browse"), QDialogButtonBox.ActionRole)
self.browseButton.setIcon(SHARED.theme.getIcon("browse"))
self.browseButton.clicked.connect(self._browseForProject)
self.btnBrowse = QPushButton(self.tr("Browse"), self)
self.btnBrowse.setIcon(SHARED.theme.getIcon("browse"))
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
# ========
@@ -129,7 +150,7 @@ class GuiWelcome(QDialog):
self.innerBox.addSpacing(hA)
self.innerBox.addWidget(self.mainStack)
self.innerBox.addSpacing(hB)
self.innerBox.addWidget(self.btnBox)
self.innerBox.addLayout(self.btnBox)
topRight = Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight
@@ -179,12 +200,14 @@ class GuiWelcome(QDialog):
def _showNewProjectPage(self) -> None:
"""Show the create new project page."""
self.mainStack.setCurrentWidget(self.tabNew)
self._setButtonVisibility()
return
@pyqtSlot()
def _showOpenProjectPage(self) -> None:
"""Show the open exiting project page."""
self.mainStack.setCurrentWidget(self.tabOpen)
self._setButtonVisibility()
return
@pyqtSlot()
@@ -195,6 +218,13 @@ class GuiWelcome(QDialog):
self._openProjectPath(path)
return
@pyqtSlot()
def _openSelectedItem(self) -> None:
"""Open the currently selected project item."""
if self.mainStack.currentWidget() == self.tabOpen:
self.tabOpen.openSelectedItem()
return
@pyqtSlot(Path)
def _openProjectPath(self, path: Path) -> None:
"""Emit a project open signal."""
@@ -216,6 +246,20 @@ class GuiWelcome(QDialog):
CONFIG.setWelcomeWinSize(self.width(), self.height())
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
@@ -445,7 +489,6 @@ class _ProjectListModel(QAbstractListModel):
class _NewProjectPage(QWidget):
cancelNewProject = pyqtSignal()
openProjectRequest = pyqtSignal(Path)
def __init__(self, parent: QWidget) -> None:
@@ -462,28 +505,11 @@ class _NewProjectPage(QWidget):
self.scrollArea.setHorizontalScrollBarPolicy(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
# ========
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(self.scrollArea)
self.outerBox.addLayout(self.buttonBox)
self.outerBox.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.outerBox)
@@ -500,11 +526,11 @@ class _NewProjectPage(QWidget):
return
##
# Private Slots
# Public Slots
##
@pyqtSlot()
def _createNewProject(self) -> None:
def createNewProject(self) -> None:
"""Create a new project from the data in the form."""
data = self.projectForm.getProjectData()
if not data.get("name"):
@@ -532,6 +558,7 @@ class _NewProjectForm(QWidget):
self._copyPath = None
iPx = SHARED.theme.baseIconSize
sPx = CONFIG.pxInt(16)
# Project Settings
# ================
@@ -547,15 +574,6 @@ class _NewProjectForm(QWidget):
self.projAuthor.setMaxLength(200)
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
self.projPath = QLineEdit(self)
self.projPath.setReadOnly(True)
@@ -601,7 +619,6 @@ class _NewProjectForm(QWidget):
self.projectForm.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.projectForm.addRow(self.tr("Project Name"), self.projName)
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("Prefill Project"), self.fillBox)
@@ -661,15 +678,23 @@ class _NewProjectForm(QWidget):
# 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.addWidget(QLabel("<b>{0}</b>".format(self.tr("Create New Project"))))
self.formBox.addLayout(self.projectForm)
self.formBox.addSpacing(16)
self.formBox.addWidget(QLabel("<b>{0}</b>".format(self.tr("Chapters and Scenes"))))
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.addSpacing(sPx)
self.formBox.addWidget(self.extraWidget)
self.formBox.addStretch(1)
self.setLayout(self.formBox)
@@ -691,7 +716,6 @@ class _NewProjectForm(QWidget):
return {
"name": self.projName.text().strip(),
"author": self.projAuthor.text().strip(),
"language": self.projLang.currentData(),
"path": self.projPath.text(),
"blank": self._fillMode == self.FILL_BLANK,
"sample": self._fillMode == self.FILL_SAMPLE,
@@ -775,14 +799,7 @@ class _NewProjectForm(QWidget):
self.projFill.setText(text)
self.projFill.setToolTip(text)
self.projFill.setCursorPosition(0)
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)
self.extraWidget.setVisible(self._fillMode == self.FILL_BLANK)
return
-2
View File
@@ -41,8 +41,6 @@ install_requires =
include = novelwriter*
[options.entry_points]
console_script =
novelwriter-cli = novelwriter:main
gui_scripts =
novelwriter = novelwriter:main
@@ -1,12 +1,12 @@
<?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">
<name>Test Project A</name>
<author>Jane Doe</author>
</project>
<settings>
<doBackup>yes</doBackup>
<language>None</language>
<language>en_GB</language>
<spellChecking auto="no">None</spellChecking>
<lastHandle>
<entry key="editor">None</entry>
@@ -1,12 +1,12 @@
<?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">
<name>Test Project B</name>
<author>Jane Doe</author>
</project>
<settings>
<doBackup>yes</doBackup>
<language>None</language>
<language>en_GB</language>
<spellChecking auto="no">None</spellChecking>
<lastHandle>
<entry key="editor">None</entry>
+8 -6
View File
@@ -509,7 +509,6 @@ def testCoreTools_ProjectBuilderCopyPlain(monkeypatch, caplog, mockGUI, prjLipsu
data = {
"name": "Test Project",
"author": "Jane Doe",
"language": "en_US",
"path": dstPath,
"template": srcPath,
}
@@ -556,9 +555,9 @@ def testCoreTools_ProjectBuilderCopyPlain(monkeypatch, caplog, mockGUI, prjLipsu
assert srcProject.data.author == "lipsum.com"
assert dstProject.data.author == "Jane Doe"
# Language should be different
# Language should be the same
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
assert dstProject.data.saveCount < 5
@@ -634,9 +633,9 @@ def testCoreTools_ProjectBuilderCopyZipped(monkeypatch, caplog, mockGUI, fncPath
assert srcProject.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 dstProject.data.language == "en_US"
assert dstProject.data.language is None
# Counts should be more or less zeroed
assert dstProject.data.saveCount < 5
@@ -652,7 +651,7 @@ def testCoreTools_ProjectBuilderSample(monkeypatch, mockGUI, fncPath, tstPaths):
data = {
"name": "Test Sample",
"author": "Jane Doe",
"path": fncPath,
"path": fncPath / "project",
"sample": True,
}
@@ -685,6 +684,9 @@ def testCoreTools_ProjectBuilderSample(monkeypatch, mockGUI, fncPath, tstPaths):
zipObj.write(docFile, f"content/{docFile.name}")
assert builder.buildProject(data) is True
# Can't create to the same target again
assert builder.buildProject(data) is False
dstSample.unlink()
# END Test testCoreTools_ProjectBuilderSample
+8 -5
View File
@@ -25,18 +25,19 @@ import pytest
from shutil import copyfile
from mocked import causeException
from novelwriter.core.item import NWItem
from tools import C, buildTestProject, cmpFiles, writeFile
from mocked import causeException
from novelwriter import SHARED
from novelwriter.enum import nwComment, nwItemClass, nwItemLayout
from novelwriter.constants import nwFiles
from novelwriter.core.item import NWItem
from novelwriter.core.index import IndexItem, NWIndex, countWords, TagsIndex, processComment
from novelwriter.core.project import NWProject
@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
the index cache file.
"""
@@ -148,8 +149,10 @@ def testCoreIndex_LoadSave(monkeypatch, prjLipsum, mockGUI, tstPaths):
assert "fb609cd8319dc" in index._itemIndex
assert "7a992350f3eb6" in index._itemIndex
# Finalise
project.closeProject()
# Close Project
with qtbot.waitSignal(SHARED.indexCleared, timeout=1000):
# Regression test for issue #1718
project.closeProject()
# END Test testCoreIndex_LoadSave
+4 -1
View File
@@ -111,12 +111,15 @@ def testCoreStorage_InitProjectStorage(mockGUI, fncPath, mockRnd):
buildTestProject(project, fncPath)
# Init with the wrong file
foo = fncPath / "foobar.txt"
foo.touch()
assert storage.initProjectStorage(fncPath / "foobar.txt") == NWStorageOpen.UNKOWN
foo.unlink()
storage._clearLockFile()
storage.clear()
# 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.clear()
+210 -10
View File
@@ -26,6 +26,7 @@ import pytest
from tools import C, buildTestProject, readFile
from novelwriter.constants import nwHeadFmt
from novelwriter.core.tomd import ToMarkdown
from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import HeadingFormatter, Tokenizer, stripEscape
@@ -1010,7 +1011,7 @@ def testCoreToken_ProcessHeaders(mockGUI):
tokens._isNote = False
##
# Story FIles
# Story Files
##
tokens._isNone = False
@@ -1122,7 +1123,7 @@ def testCoreToken_ProcessHeaders(mockGUI):
# H3: Scene wo/Format, first
tokens._text = "### Scene One\n"
tokens.setSceneFormat("", False)
tokens._firstScene = True
tokens._skipSeparator = True
tokens.tokenizeText()
tokens.doHeaders()
assert tokens._tokens == [
@@ -1133,7 +1134,7 @@ def testCoreToken_ProcessHeaders(mockGUI):
# H3: Scene wo/Format, not first
tokens._text = "### Scene One\n"
tokens.setSceneFormat("", False)
tokens._firstScene = False
tokens._skipSeparator = False
tokens.tokenizeText()
tokens.doHeaders()
assert tokens._tokens == [
@@ -1144,7 +1145,7 @@ def testCoreToken_ProcessHeaders(mockGUI):
# H3: Scene Separator, first
tokens._text = "### Scene One\n"
tokens.setSceneFormat("* * *", False)
tokens._firstScene = True
tokens._skipSeparator = True
tokens.tokenizeText()
tokens.doHeaders()
assert tokens._tokens == [
@@ -1155,7 +1156,7 @@ def testCoreToken_ProcessHeaders(mockGUI):
# H3: Scene Separator, not first
tokens._text = "### Scene One\n"
tokens.setSceneFormat("* * *", False)
tokens._firstScene = False
tokens._skipSeparator = False
tokens.tokenizeText()
tokens.doHeaders()
assert tokens._tokens == [
@@ -1192,7 +1193,7 @@ def testCoreToken_ProcessHeaders(mockGUI):
# H4: Section Hidden wo/Format
tokens._text = "#### A Section\n"
tokens.setSectionFormat(r"", True)
tokens.setSectionFormat("", True)
tokens.tokenizeText()
tokens.doHeaders()
assert tokens._tokens == [
@@ -1230,17 +1231,210 @@ def testCoreToken_ProcessHeaders(mockGUI):
(Tokenizer.T_EMPTY, 1, "", [], Tokenizer.A_NONE),
]
# Check the first scene detector
assert tokens._firstScene is False
tokens._firstScene = True
# Check the first scene detector, plain text
tokens._skipSeparator = True
tokens._text = "Some text ...\n"
tokens.tokenizeText()
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
@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
def testCoreIndex_HeadingFormatter(fncPath, mockRnd):
"""Check the HeadingFormatter class."""
@@ -1306,6 +1500,12 @@ def testCoreIndex_HeadingFormatter(fncPath, mockRnd):
formatter.incScene()
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
# ===============
formatter._chCount = 2
+41 -8
View File
@@ -23,30 +23,63 @@ from __future__ import annotations
import sys
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)
@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.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."""
monkeypatch.setattr(QDialog, "exec_", lambda *a: None)
monkeypatch.setattr(QMessageBox, "exec_", lambda *a: None)
monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.Yes)
# Set the test langauge
# Set the test language
CONFIG.guiLocale = language
CONFIG.initLocalisation(qApp)
nwGUI = main(["--testmode", f"--config={fncPath}", f"--data={fncPath}"])
qtbot.addWidget(nwGUI)
buildTestProject(nwGUI, projPath)
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)
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[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()
# END Test testGuiProjTree_ContextMenu
+3 -2
View File
@@ -141,11 +141,12 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI):
mp.setattr("builtins.open", causeOSError)
assert mainTheme.listThemes() == []
# Load the theme info
# Load the theme info, default themes first
themesList = mainTheme.listThemes()
assert themesList[0] == ("default_dark", "Default Dark 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
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)
nwGUI.openProject(projPath)
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)
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
# Show the new project form
welcome.newButton.click()
welcome.btnNew.click()
assert welcome.mainStack.currentIndex() == 1
# Revert to project lits
welcome.tabNew.cancelNewProject.emit()
welcome.btnList.click()
assert welcome.mainStack.currentIndex() == 0
# Open a project
with monkeypatch.context() as mp:
mp.setattr(SHARED, "getProjectPath", lambda *a, **k: fncPath)
with qtbot.waitSignal(welcome.openProjectRequest) as signal:
welcome.browseButton.click()
welcome.btnBrowse.click()
assert signal.args and signal.args[0] == fncPath
# qtbot.stop()
@@ -103,7 +103,7 @@ def testToolWelcome_Open(qtbot: QtBot, monkeypatch, nwGUI, fncPath):
qtbot.mouseClick(vPort, Qt.MouseButton.LeftButton, pos=posTwo, delay=10)
assert tabOpen.selectedPath.text() == "Path: /stuff/project_one"
# Double Click item
# Double click item
qtbot.mouseClick(vPort, Qt.MouseButton.LeftButton, pos=posTwo, delay=10)
with monkeypatch.context() as mp:
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)
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
def getMenuForPos(pos: QPoint) -> QMenu | None:
nonlocal tabOpen
@@ -165,7 +173,7 @@ def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath):
with qtbot.waitExposed(welcome):
welcome.show()
welcome.newButton.click()
welcome.btnNew.click()
assert welcome.mainStack.currentIndex() == 1
tabNew = welcome.tabNew
newForm = tabNew.projectForm
@@ -188,12 +196,7 @@ def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath):
newForm.fillSample.trigger()
assert newForm._fillMode == newForm.FILL_SAMPLE
assert newForm.projFill.text() == "Example Project"
assert newForm.addNotes.isEnabled() 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
assert newForm.extraWidget.isVisible() is False
# Change fill info to template
with monkeypatch.context() as mp:
@@ -201,12 +204,7 @@ def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath):
newForm.fillCopy.trigger()
assert newForm._fillMode == newForm.FILL_COPY
assert newForm.projFill.text() == f"Template: {fncPath}"
assert newForm.addNotes.isEnabled() 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
assert newForm.extraWidget.isVisible() is False
# Change back to fill blank using the menu
newForm.browseFill.click()
@@ -215,16 +213,11 @@ def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath):
newForm.fillMenu.close()
assert newForm._fillMode == newForm.FILL_BLANK
assert newForm.projFill.text() == "Fresh Project"
assert newForm.addNotes.isEnabled() 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
assert newForm.extraWidget.isVisible() is True
# Creating a project without a name, pops an error
caplog.clear()
tabNew.createButton.click()
welcome.btnCreate.click()
assert "A project name is required." in caplog.text
# Set some more values, and extract data
@@ -240,7 +233,6 @@ def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath):
assert newForm.getProjectData() == {
"name": "Test Project",
"author": "Jane Smith",
"language": "en_GB",
"path": str(projPath),
"blank": True,
"sample": False,
@@ -253,7 +245,7 @@ def testToolWelcome_New(qtbot: QtBot, caplog, monkeypatch, nwGUI, fncPath):
# Create a project with these values
with qtbot.waitSignal(welcome.openProjectRequest, timeout=5000) as signal:
tabNew.createButton.click()
welcome.btnCreate.click()
assert signal.args and signal.args[0] == projPath
assert (projPath / nwFiles.PROJ_FILE).exists()