Merge branch 'main' into update_docs
This commit is contained in:
@@ -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
File diff suppressed because it is too large
Load Diff
+1658
-1781
File diff suppressed because it is too large
Load Diff
+1658
-1781
File diff suppressed because it is too large
Load Diff
+1657
-1780
File diff suppressed because it is too large
Load Diff
+1657
-1780
File diff suppressed because it is too large
Load Diff
+2198
-2139
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user