Add outline to manuscript tool (#1768)

This commit is contained in:
Veronica Berglyd Olsen
2024-03-21 18:49:55 +01:00
committed by GitHub
34 changed files with 610 additions and 457 deletions
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m10.837 12.256h-6.8373v-6.8347c0.010667-1.5131 0.89467-1.877 1.964-0.80652l1.5387 1.5437c1.3533-1.0598 3.0093-1.6397 4.7547-1.6397 2.0667 0 4.012 0.80518 5.472 2.2676 1.4653 1.4557 2.2707 3.4034 2.2707 5.4696 0 2.0703-0.80533 4.0153-2.268 5.475-1.4627 1.4624-3.4067 2.2689-5.4747 2.2689-2.0667 0-4.012-0.80652-5.4747-2.2702-0.39467-0.39593-0.744-0.82784-1.04-1.2864-0.46267-0.7212-0.25333-1.6797 0.46667-2.1396 0.71867-0.46125 1.6773-0.25195 2.1387 0.46658 0.17733 0.27595 0.38933 0.53323 0.624 0.77319 0.87867 0.87717 2.0453 1.3597 3.2853 1.3597s2.4067-0.48124 3.2853-1.3597c0.876-0.87717 1.36-2.0436 1.36-3.2861 0-1.2398-0.48267-2.4062-1.36-3.2807-0.87867-0.87717-2.0453-1.3611-3.2853-1.3611-0.91733 0-1.7947 0.26795-2.5453 0.76252l1.9307 1.9343c1.0707 1.0691 0.70667 1.9436-0.80533 1.9436z" fill="#9c9" stroke-width="1.3332"/> <path d="m10.692 12.288h-7.692v-7.689c0.012-1.7022 1.0065-2.1116 2.2095-0.90734l1.731 1.7367c1.5225-1.1923 3.3855-1.8447 5.349-1.8447 2.325 0 4.5135 0.90583 6.156 2.5511 1.6485 1.6377 2.5545 3.8288 2.5545 6.1533 0 2.3291-0.906 4.5172-2.5515 6.1594-1.6455 1.6452-3.8325 2.5525-6.159 2.5525-2.325 0-4.5135-0.90734-6.159-2.554-0.444-0.44542-0.837-0.93132-1.17-1.4472-0.5205-0.81135-0.285-1.8897 0.525-2.4071 0.8085-0.51891 1.887-0.28344 2.406 0.5249 0.1995 0.31044 0.438 0.59988 0.702 0.86984 0.9885 0.98682 2.301 1.5297 3.696 1.5297 1.395 0 2.7075-0.5414 3.696-1.5297 0.9855-0.98682 1.53-2.2991 1.53-3.6969 0-1.3948-0.543-2.707-1.53-3.6908-0.9885-0.98682-2.301-1.5312-3.696-1.5312-1.032 0-2.019 0.30144-2.8635 0.85784l2.172 2.1761c1.2045 1.2027 0.795 2.1866-0.906 2.1866z" fill="#9c9" stroke-width="1.3332"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 984 B

After

Width:  |  Height:  |  Size: 952 B

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m13.163 12.256h6.8373v-6.8347c-0.010667-1.5131-0.89467-1.877-1.964-0.80652l-1.5387 1.5437c-1.3533-1.0598-3.0093-1.6397-4.7547-1.6397-2.0667 0-4.012 0.80518-5.472 2.2676-1.4653 1.4557-2.2707 3.4034-2.2707 5.4696 0 2.0703 0.80533 4.0153 2.268 5.475 1.4627 1.4624 3.4067 2.2689 5.4747 2.2689 2.0667 0 4.012-0.80652 5.4747-2.2702 0.39467-0.39593 0.744-0.82784 1.04-1.2864 0.46267-0.7212 0.25333-1.6797-0.46667-2.1396-0.71867-0.46125-1.6773-0.25195-2.1387 0.46658-0.17733 0.27595-0.38933 0.53323-0.624 0.77319-0.87867 0.87717-2.0453 1.3597-3.2853 1.3597s-2.4067-0.48124-3.2853-1.3597c-0.876-0.87717-1.36-2.0436-1.36-3.2861 0-1.2398 0.48267-2.4062 1.36-3.2807 0.87867-0.87717 2.0453-1.3611 3.2853-1.3611 0.91733 0 1.7947 0.26795 2.5453 0.76252l-1.9307 1.9343c-1.0707 1.0691-0.70667 1.9436 0.80533 1.9436z" fill="#9c9" stroke-width="1.3332"/> <path d="m13.308 12.288h7.692v-7.689c-0.012-1.7022-1.0065-2.1116-2.2095-0.90734l-1.731 1.7367c-1.5225-1.1923-3.3855-1.8447-5.349-1.8447-2.325 0-4.5135 0.90583-6.156 2.5511-1.6485 1.6377-2.5545 3.8288-2.5545 6.1533 0 2.3291 0.906 4.5172 2.5515 6.1594 1.6455 1.6452 3.8325 2.5525 6.159 2.5525 2.325 0 4.5135-0.90734 6.159-2.554 0.444-0.44542 0.837-0.93132 1.17-1.4472 0.5205-0.81135 0.285-1.8897-0.525-2.4071-0.8085-0.51891-1.887-0.28344-2.406 0.5249-0.1995 0.31044-0.438 0.59988-0.702 0.86984-0.9885 0.98682-2.301 1.5297-3.696 1.5297-1.395 0-2.7075-0.5414-3.696-1.5297-0.9855-0.98682-1.53-2.2991-1.53-3.6969 0-1.3948 0.543-2.707 1.53-3.6908 0.9885-0.98682 2.301-1.5312 3.696-1.5312 1.032 0 2.019 0.30144 2.8635 0.85784l-2.172 2.1761c-1.2045 1.2027-0.795 2.1866 0.906 2.1866z" fill="#9c9" stroke-width="1.3332"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 990 B

After

Width:  |  Height:  |  Size: 956 B

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m19.041 4.9586c-1.0144-1.0157-2.6635-1.0157-3.678 0l-3.3632 3.3632-3.3632-3.3632c-1.0144-1.0157-2.6635-1.0157-3.678 0-1.0157 1.0157-1.0157 2.6622 0 3.678l3.3619 3.3632-3.3619 3.3632c-1.0157 1.0157-1.0157 2.6622 0 3.678 0.50722 0.50852 1.1731 0.76212 1.839 0.76212 0.66588 0 1.3318-0.25361 1.839-0.76212l3.3632-3.3632 3.3632 3.3632c0.50722 0.50852 1.1731 0.76212 1.839 0.76212 0.66588 0 1.3318-0.25361 1.839-0.76212 1.0157-1.0157 1.0157-2.6622 0-3.678l-3.3619-3.3632 3.3619-3.3632c1.0157-1.0157 1.0157-2.6622 0-3.678z" fill="#d64848" stroke-width="1.3006"/> <path d="m20.121 3.8786c-1.17-1.1715-3.072-1.1715-4.2421 0l-3.879 3.8791-3.879-3.8791c-1.17-1.1715-3.072-1.1715-4.2421 0-1.1715 1.1715-1.1715 3.0705 0 4.2421l3.8776 3.879-3.8776 3.879c-1.1715 1.1715-1.1715 3.0705 0 4.2421 0.58502 0.58652 1.353 0.87902 2.1211 0.87902 0.76801 0 1.5361-0.29251 2.1211-0.87902l3.879-3.879 3.879 3.879c0.58502 0.58652 1.353 0.87902 2.1211 0.87902 0.76801 0 1.5361-0.29251 2.1211-0.87902 1.1715-1.1715 1.1715-3.0705 0-4.2421l-3.8776-3.879 3.8776-3.879c1.1715-1.1715 1.1715-3.0705 0-4.2421z" fill="#d64848" stroke-width="1.5001"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 711 B

After

Width:  |  Height:  |  Size: 703 B

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m10.837 12.256h-6.8373v-6.8347c0.010667-1.5131 0.89467-1.877 1.964-0.80652l1.5387 1.5437c1.3533-1.0598 3.0093-1.6397 4.7547-1.6397 2.0667 0 4.012 0.80518 5.472 2.2676 1.4653 1.4557 2.2707 3.4034 2.2707 5.4696 0 2.0703-0.80533 4.0153-2.268 5.475-1.4627 1.4624-3.4067 2.2689-5.4747 2.2689-2.0667 0-4.012-0.80652-5.4747-2.2702-0.39467-0.39593-0.744-0.82784-1.04-1.2864-0.46267-0.7212-0.25333-1.6797 0.46667-2.1396 0.71867-0.46125 1.6773-0.25195 2.1387 0.46658 0.17733 0.27595 0.38933 0.53323 0.624 0.77319 0.87867 0.87717 2.0453 1.3597 3.2853 1.3597s2.4067-0.48124 3.2853-1.3597c0.876-0.87717 1.36-2.0436 1.36-3.2861 0-1.2398-0.48267-2.4062-1.36-3.2807-0.87867-0.87717-2.0453-1.3611-3.2853-1.3611-0.91733 0-1.7947 0.26795-2.5453 0.76252l1.9307 1.9343c1.0707 1.0691 0.70667 1.9436-0.80533 1.9436z" fill="#718c00" stroke-width="1.3332"/> <path d="m10.692 12.288h-7.692v-7.689c0.012-1.7022 1.0065-2.1116 2.2095-0.90734l1.731 1.7367c1.5225-1.1923 3.3855-1.8447 5.349-1.8447 2.325 0 4.5135 0.90583 6.156 2.5511 1.6485 1.6377 2.5545 3.8288 2.5545 6.1533 0 2.3291-0.906 4.5172-2.5515 6.1594-1.6455 1.6452-3.8325 2.5525-6.159 2.5525-2.325 0-4.5135-0.90734-6.159-2.554-0.444-0.44542-0.837-0.93132-1.17-1.4472-0.5205-0.81135-0.285-1.8897 0.525-2.4071 0.8085-0.51891 1.887-0.28344 2.406 0.5249 0.1995 0.31044 0.438 0.59988 0.702 0.86984 0.9885 0.98682 2.301 1.5297 3.696 1.5297 1.395 0 2.7075-0.5414 3.696-1.5297 0.9855-0.98682 1.53-2.2991 1.53-3.6969 0-1.3948-0.543-2.707-1.53-3.6908-0.9885-0.98682-2.301-1.5312-3.696-1.5312-1.032 0-2.019 0.30144-2.8635 0.85784l2.172 2.1761c1.2045 1.2027 0.795 2.1866-0.906 2.1866z" fill="#718c00" stroke-width="1.4998"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 987 B

After

Width:  |  Height:  |  Size: 955 B

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m13.163 12.256h6.8373v-6.8347c-0.010667-1.5131-0.89467-1.877-1.964-0.80652l-1.5387 1.5437c-1.3533-1.0598-3.0093-1.6397-4.7547-1.6397-2.0667 0-4.012 0.80518-5.472 2.2676-1.4653 1.4557-2.2707 3.4034-2.2707 5.4696 0 2.0703 0.80533 4.0153 2.268 5.475 1.4627 1.4624 3.4067 2.2689 5.4747 2.2689 2.0667 0 4.012-0.80652 5.4747-2.2702 0.39467-0.39593 0.744-0.82784 1.04-1.2864 0.46267-0.7212 0.25333-1.6797-0.46667-2.1396-0.71867-0.46125-1.6773-0.25195-2.1387 0.46658-0.17733 0.27595-0.38933 0.53323-0.624 0.77319-0.87867 0.87717-2.0453 1.3597-3.2853 1.3597s-2.4067-0.48124-3.2853-1.3597c-0.876-0.87717-1.36-2.0436-1.36-3.2861 0-1.2398 0.48267-2.4062 1.36-3.2807 0.87867-0.87717 2.0453-1.3611 3.2853-1.3611 0.91733 0 1.7947 0.26795 2.5453 0.76252l-1.9307 1.9343c-1.0707 1.0691-0.70667 1.9436 0.80533 1.9436z" fill="#718c00" stroke-width="1.3332"/> <path d="m13.308 12.288h7.692v-7.689c-0.012001-1.7022-1.0065-2.1116-2.2095-0.90734l-1.731 1.7367c-1.5225-1.1923-3.3855-1.8447-5.349-1.8447-2.325 0-4.5135 0.90583-6.156 2.5511-1.6485 1.6377-2.5545 3.8288-2.5545 6.1533 0 2.3291 0.906 4.5172 2.5515 6.1594 1.6455 1.6452 3.8325 2.5525 6.159 2.5525 2.325 0 4.5135-0.90734 6.159-2.554 0.444-0.44542 0.837-0.93132 1.17-1.4472 0.5205-0.81135 0.285-1.8897-0.525-2.4071-0.8085-0.51891-1.887-0.28344-2.406 0.5249-0.1995 0.31044-0.438 0.59988-0.702 0.86984-0.9885 0.98682-2.301 1.5297-3.696 1.5297-1.395 0-2.7075-0.5414-3.696-1.5297-0.9855-0.98682-1.53-2.2991-1.53-3.6969 0-1.3948 0.543-2.707 1.53-3.6908 0.9885-0.98682 2.301-1.5312 3.696-1.5312 1.032 0 2.019 0.30144 2.8635 0.85784l-2.172 2.1761c-1.2045 1.2027-0.795 2.1866 0.906 2.1866z" fill="#718c00" stroke-width="1.4998"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 993 B

After

Width:  |  Height:  |  Size: 962 B

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m19.041 4.9586c-1.0144-1.0157-2.6635-1.0157-3.678 0l-3.3632 3.3632-3.3632-3.3632c-1.0144-1.0157-2.6635-1.0157-3.678 0-1.0157 1.0157-1.0157 2.6622 0 3.678l3.3619 3.3632-3.3619 3.3632c-1.0157 1.0157-1.0157 2.6622 0 3.678 0.50722 0.50852 1.1731 0.76212 1.839 0.76212 0.66588 0 1.3318-0.25361 1.839-0.76212l3.3632-3.3632 3.3632 3.3632c0.50722 0.50852 1.1731 0.76212 1.839 0.76212 0.66588 0 1.3318-0.25361 1.839-0.76212 1.0157-1.0157 1.0157-2.6622 0-3.678l-3.3619-3.3632 3.3619-3.3632c1.0157-1.0157 1.0157-2.6622 0-3.678z" fill="#c82829" stroke-width="1.3006"/> <path d="m20.121 3.8786c-1.17-1.1715-3.072-1.1715-4.2421 0l-3.879 3.8791-3.879-3.8791c-1.17-1.1715-3.072-1.1715-4.2421 0-1.1715 1.1715-1.1715 3.0705 0 4.2421l3.8776 3.879-3.8776 3.879c-1.1715 1.1715-1.1715 3.0705 0 4.2421 0.58502 0.58652 1.353 0.87902 2.1211 0.87902 0.76801 0 1.5361-0.29251 2.1211-0.87902l3.879-3.879 3.879 3.879c0.58502 0.58652 1.353 0.87902 2.1211 0.87902 0.76801 0 1.5361-0.29251 2.1211-0.87902 1.1715-1.1715 1.1715-3.0705 0-4.2421l-3.8776-3.879 3.8776-3.879c1.1715-1.1715 1.1715-3.0705 0-4.2421z" fill="#c82829" stroke-width="1.5001"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 711 B

After

Width:  |  Height:  |  Size: 703 B

+21 -3
View File
@@ -52,7 +52,10 @@ class NWBuildDocument:
manuscript, based on a build definition object (BuildSettings). manuscript, based on a build definition object (BuildSettings).
""" """
__slots__ = ("_project", "_build", "_queue", "_error", "_cache", "_count", "_preview") __slots__ = (
"_project", "_build", "_queue", "_error", "_cache", "_count",
"_outline", "_preview"
)
def __init__(self, project: NWProject, build: BuildSettings) -> None: def __init__(self, project: NWProject, build: BuildSettings) -> None:
self._project = project self._project = project
@@ -61,6 +64,7 @@ class NWBuildDocument:
self._error = None self._error = None
self._cache = None self._cache = None
self._count = False self._count = False
self._outline = False
self._preview = False self._preview = False
return return
@@ -86,13 +90,21 @@ class NWBuildDocument:
## ##
def setCountEnabled(self, state: bool) -> None: def setCountEnabled(self, state: bool) -> None:
"""Turn on/off stats counting for builds.""" """Turn on/off stats for builds."""
self._count = state self._count = state
return return
def setBuildOutline(self, state: bool) -> None:
"""Turn on/off outline for builds."""
self._outline = state
return
def setPreviewMode(self, state: bool) -> None: def setPreviewMode(self, state: bool) -> None:
"""Set the preview mode of the build. Implies count mode.""" """Set the preview mode of the build. This also enables stats
count and outline mode.
"""
self._preview = state self._preview = state
self._outline = state
self._count = state self._count = state
return return
@@ -170,6 +182,8 @@ class NWBuildDocument:
makeObj = ToHtml(self._project) makeObj = ToHtml(self._project)
filtered = self._setupBuild(makeObj) filtered = self._setupBuild(makeObj)
makeObj.setPreview(self._preview)
makeObj.setLinkHeadings(self._preview)
for i, tHandle in enumerate(self._queue): for i, tHandle in enumerate(self._queue):
self._error = None self._error = None
if filtered.get(tHandle, (False, 0))[0]: if filtered.get(tHandle, (False, 0))[0]:
@@ -362,12 +376,16 @@ class NWBuildDocument:
bldObj.doConvert() bldObj.doConvert()
if self._count: if self._count:
bldObj.countStats() bldObj.countStats()
if self._outline:
bldObj.buildOutline()
elif tItem.isFileType(): elif tItem.isFileType():
bldObj.setText(tHandle) bldObj.setText(tHandle)
bldObj.doPreProcessing() bldObj.doPreProcessing()
bldObj.tokenizeText() bldObj.tokenizeText()
if self._count: if self._count:
bldObj.countStats() bldObj.countStats()
if self._outline:
bldObj.buildOutline()
if convert: if convert:
bldObj.doConvert() bldObj.doConvert()
else: else:
+4 -4
View File
@@ -33,7 +33,7 @@ import logging
from time import time from time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from collections.abc import Generator, ItemsView, Iterable from collections.abc import ItemsView, Iterable
from novelwriter import SHARED from novelwriter import SHARED
from novelwriter.enum import nwComment, nwItemClass, nwItemType, nwItemLayout from novelwriter.enum import nwComment, nwItemClass, nwItemType, nwItemLayout
@@ -523,7 +523,7 @@ class NWIndex:
return tItem[sTitle] return tItem[sTitle]
return None return None
def iterItemHeadings(self, tHandle: str) -> Generator[str, IndexHeading]: def iterItemHeadings(self, tHandle: str) -> Iterable[tuple[str, IndexHeading]]:
"""Get all headings for a specific item.""" """Get all headings for a specific item."""
if tItem := self._itemIndex[tHandle]: if tItem := self._itemIndex[tHandle]:
yield from tItem.items() yield from tItem.items()
@@ -531,7 +531,7 @@ class NWIndex:
def novelStructure( def novelStructure(
self, rootHandle: str | None = None, activeOnly: bool = True self, rootHandle: str | None = None, activeOnly: bool = True
) -> Generator[tuple[str, str, str, IndexHeading]]: ) -> Iterable[tuple[str, str, str, IndexHeading]]:
"""Iterate over all titles in the novel, in the correct order as """Iterate over all titles in the novel, in the correct order as
they appear in the tree view and in the respective document they appear in the tree view and in the respective document
files, but skipping all note files. files, but skipping all note files.
@@ -673,7 +673,7 @@ class NWIndex:
def getTagsData( def getTagsData(
self, activeOnly: bool = True self, activeOnly: bool = True
) -> Generator[tuple[str, str, str, IndexItem | None, IndexHeading | None]]: ) -> Iterable[tuple[str, str, str, IndexItem | None, IndexHeading | None]]:
"""Return all known tags.""" """Return all known tags."""
for tag, data in self._tagsIndex.items(): for tag, data in self._tagsIndex.items():
iItem = self._itemIndex[data.get("handle")] iItem = self._itemIndex[data.get("handle")]
+2 -2
View File
@@ -31,7 +31,7 @@ from time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from functools import partial from functools import partial
from collections.abc import Generator from collections.abc import Iterable
from PyQt5.QtCore import QCoreApplication from PyQt5.QtCore import QCoreApplication
@@ -517,7 +517,7 @@ class NWProject:
# Class Methods # Class Methods
## ##
def iterProjectItems(self) -> Generator[NWItem]: def iterProjectItems(self) -> Iterable[NWItem]:
"""This function ensures that the item tree loaded is sent to """This function ensures that the item tree loaded is sent to
the GUI tree view in such a way that the tree can be built. That the GUI tree view in such a way that the tree can be built. That
is, the parent item must be sent before its child. In principle, is, the parent item must be sent before its child. In principle,
+2 -2
View File
@@ -29,7 +29,7 @@ import logging
from time import time from time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from collections.abc import Generator from collections.abc import Iterable
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import formatTimeStamp from novelwriter.common import formatTimeStamp
@@ -110,7 +110,7 @@ class NWSessionLog:
return True return True
def iterRecords(self) -> Generator[dict]: def iterRecords(self) -> Iterable[dict]:
"""Iterate through all records in the log.""" """Iterate through all records in the log."""
sessFile = self._project.storage.getMetaFile(nwFiles.SESS_FILE) sessFile = self._project.storage.getMetaFile(nwFiles.SESS_FILE)
if isinstance(sessFile, Path) and sessFile.is_file(): if isinstance(sessFile, Path) and sessFile.is_file():
+4 -4
View File
@@ -28,10 +28,10 @@ import random
import logging import logging
from typing import TYPE_CHECKING, Literal from typing import TYPE_CHECKING, Literal
from collections.abc import Generator, ItemsView, Iterator, KeysView, ValuesView from collections.abc import ItemsView, Iterable, Iterator, KeysView, ValuesView
from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor
from PyQt5.QtCore import QRectF, Qt from PyQt5.QtCore import QRectF
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.common import minmax, simplified from novelwriter.common import minmax, simplified
@@ -193,7 +193,7 @@ class NWStatus:
self._store[key]["count"] += 1 self._store[key]["count"] += 1
return return
def pack(self) -> Generator[tuple[str, dict]]: def pack(self) -> Iterable[tuple[str, dict]]:
"""Pack the status entries into a dictionary.""" """Pack the status entries into a dictionary."""
for key, data in self._store.items(): for key, data in self._store.items():
yield (data["name"], { yield (data["name"], {
@@ -248,7 +248,7 @@ class NWStatus:
def _createIcon(self, red: int, green: int, blue: int) -> QIcon: def _createIcon(self, red: int, green: int, blue: int) -> QIcon:
"""Generate an icon for a status label.""" """Generate an icon for a status label."""
pixmap = QPixmap(self._iPX, self._iPX) pixmap = QPixmap(self._iPX, self._iPX)
pixmap.fill(Qt.transparent) pixmap.fill(QColor(0, 0, 0, 0))
painter = QPainter(pixmap) painter = QPainter(pixmap)
painter.setRenderHint(QPainter.Antialiasing) painter.setRenderHint(QPainter.Antialiasing)
+8 -13
View File
@@ -74,15 +74,9 @@ class ToHtml(Tokenizer):
# Setters # Setters
## ##
def setPreview(self, doComments: bool, doSynopsis: bool) -> None: def setPreview(self, state: bool) -> None:
"""If we're using this class to generate markdown preview, we """Set to preview generator mode."""
need to make a few changes to formatting, which is managed by self._genMode = self.M_PREVIEW if state else self.M_EXPORT
these flags.
"""
self._genMode = self.M_PREVIEW
self._doKeywords = True
self._doComments = doComments
self._doSynopsis = doSynopsis
return return
def setStyles(self, cssStyles: bool) -> None: def setStyles(self, cssStyles: bool) -> None:
@@ -175,6 +169,8 @@ class ToHtml(Tokenizer):
pStyle = None pStyle = None
lines = [] lines = []
tHandle = self._handle
for tType, nHead, tText, tFormat, tStyle in self._tokens: for tType, nHead, tText, tFormat, tStyle in self._tokens:
# Replace < and > with HTML entities # Replace < and > with HTML entities
@@ -233,8 +229,8 @@ class ToHtml(Tokenizer):
else: else:
hStyle = "" hStyle = ""
if self._linkHeaders: if self._linkHeadings and tHandle:
aNm = f"<a name='T{nHead:04d}'></a>" aNm = f"<a name='{tHandle}:T{nHead:04d}'></a>"
else: else:
aNm = "" aNm = ""
@@ -302,8 +298,7 @@ class ToHtml(Tokenizer):
lines.append(tTemp) lines.append(tTemp)
self._result = "".join(lines) self._result = "".join(lines)
if self._genMode != self.M_PREVIEW: self._fullHTML.append(self._result)
self._fullHTML.append(self._result)
return return
+67 -45
View File
@@ -35,13 +35,13 @@ from functools import partial
from PyQt5.QtCore import QCoreApplication, QRegularExpression from PyQt5.QtCore import QCoreApplication, QRegularExpression
from novelwriter.enum import nwComment, nwItemLayout
from novelwriter.common import formatTimeStamp, numberToRoman, checkInt from novelwriter.common import formatTimeStamp, numberToRoman, checkInt
from novelwriter.constants import ( from novelwriter.constants import (
nwHeadFmt, nwKeyWords, nwLabels, nwRegEx, nwShortcode, nwUnicode, trConst nwHeadFmt, nwKeyWords, nwLabels, nwRegEx, nwShortcode, nwUnicode, trConst
) )
from novelwriter.core.index import processComment from novelwriter.core.index import processComment
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.enum import nwComment, nwItemLayout
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -118,15 +118,16 @@ class Tokenizer(ABC):
# Data Variables # Data Variables
self._text = "" # The raw text to be tokenized self._text = "" # The raw text to be tokenized
self._nwItem = None # The NWItem currently being processed self._handle = None # The item handle currently being processed
self._result = "" # The result of the last document self._result = "" # The result of the last document
self._counts = {} # Counter data
self._keepMarkdown = False # Whether to keep the markdown text self._keepMarkdown = False # Whether to keep the markdown text
self._allMarkdown = [] # The result novelWriter markdown of all documents self._allMarkdown = [] # The result novelWriter markdown of all documents
# Processed Tokens # Processed Tokens and Meta Data
self._tokens: list[tuple[int, int, str, list[tuple[int, int]], int]] = [] self._tokens: list[tuple[int, int, str, list[tuple[int, int]], int]] = []
self._counts: dict[str, int] = {}
self._outline: dict[str, str] = {}
# User Settings # User Settings
self._textFont = "Serif" # Output text font self._textFont = "Serif" # Output text font
@@ -165,7 +166,7 @@ class Tokenizer(ABC):
self._hideHScene = False # Do not include hard scene headings self._hideHScene = False # Do not include hard scene headings
self._hideSection = False # Do not include section headings self._hideSection = False # Do not include section headings
self._linkHeaders = False # Add an anchor before headings self._linkHeadings = False # Add an anchor before headings
self._titleStyle = self.A_CENTRE | self.A_PBB self._titleStyle = self.A_CENTRE | self.A_PBB
self._chapterStyle = self.A_PBB self._chapterStyle = self.A_PBB
@@ -176,9 +177,7 @@ class Tokenizer(ABC):
self._noSep = True # Flag to indicate that we don't want a scene separator self._noSep = True # Flag to indicate that we don't want a scene separator
# This File # This File
self._isNone = False # Document has unknown layout
self._isNovel = False # Document is a novel document self._isNovel = False # Document is a novel document
self._isNote = False # Document is a project note
self._isFirst = True # Document is the first in a set self._isFirst = True # Document is the first in a set
# Error Handling # Error Handling
@@ -228,6 +227,11 @@ class Tokenizer(ABC):
"""The collected stats about the text.""" """The collected stats about the text."""
return self._counts return self._counts
@property
def textOutline(self) -> dict[str, str]:
"""The generated outline of the text."""
return self._outline
@property @property
def errData(self) -> list[str]: def errData(self) -> list[str]:
"""The error data.""" """The error data."""
@@ -353,7 +357,7 @@ class Tokenizer(ABC):
def setLinkHeadings(self, state: bool) -> None: def setLinkHeadings(self, state: bool) -> None:
"""Enable or disable adding an anchor before headings.""" """Enable or disable adding an anchor before headings."""
self._linkHeaders = state self._linkHeadings = state
return return
def setBodyText(self, state: bool) -> None: def setBodyText(self, state: bool) -> None:
@@ -394,47 +398,43 @@ class Tokenizer(ABC):
def doConvert(self) -> None: def doConvert(self) -> None:
raise NotImplementedError raise NotImplementedError
def addRootHeading(self, tHandle: str) -> bool: def addRootHeading(self, tHandle: str) -> None:
"""Add a heading at the start of a new root folder.""" """Add a heading at the start of a new root folder."""
tItem = self._project.tree[tHandle] self._text = ""
if not tItem or not tItem.isRootType(): self._handle = None
return False
if self._isFirst: if (tItem := self._project.tree[tHandle]) and tItem.isRootType():
textAlign = self.A_CENTRE self._handle = tHandle
self._isFirst = False if self._isFirst:
else: textAlign = self.A_CENTRE
textAlign = self.A_PBB | self.A_CENTRE self._isFirst = False
else:
textAlign = self.A_PBB | self.A_CENTRE
trNotes = self._localLookup("Notes") trNotes = self._localLookup("Notes")
title = f"{trNotes}: {tItem.itemName}" title = f"{trNotes}: {tItem.itemName}"
self._tokens = [] self._tokens = []
self._tokens.append(( self._tokens.append((
self.T_TITLE, 0, title, [], textAlign self.T_TITLE, 1, title, [], textAlign
)) ))
if self._keepMarkdown: if self._keepMarkdown:
self._allMarkdown.append(f"# {title}\n\n") self._allMarkdown.append(f"#! {title}\n\n")
return True return
def setText(self, tHandle: str, text: str | None = None) -> bool: def setText(self, tHandle: str, text: str | None = None) -> None:
"""Set the text for the tokenizer from a handle. If text is not """Set the text for the tokenizer from a handle. If text is not
set, load it from the file. set, it's is loaded from the file.
""" """
self._nwItem = self._project.tree[tHandle] self._text = ""
if self._nwItem is None: self._handle = None
return False if nwItem := self._project.tree[tHandle]:
if text is None:
if text is None: text = self._project.storage.getDocument(tHandle).readDocument() or ""
text = self._project.storage.getDocument(tHandle).readDocument() or "" self._text = text
self._handle = tHandle
self._text = text self._isNovel = nwItem.itemLayout == nwItemLayout.DOCUMENT
return
self._isNone = self._nwItem.itemLayout == nwItemLayout.NO_LAYOUT
self._isNovel = self._nwItem.itemLayout == nwItemLayout.DOCUMENT
self._isNote = self._nwItem.itemLayout == nwItemLayout.NOTE
return True
def doPreProcessing(self) -> None: def doPreProcessing(self) -> None:
"""Run trough the various replace dictionaries.""" """Run trough the various replace dictionaries."""
@@ -470,7 +470,7 @@ class Tokenizer(ABC):
""" """
self._tokens = [] self._tokens = []
if self._isNovel: if self._isNovel:
self._hFormatter.setHandle(self._nwItem.itemHandle if self._nwItem else None) self._hFormatter.setHandle(self._handle)
nHead = 0 nHead = 0
breakNext = False breakNext = False
@@ -803,7 +803,29 @@ class Tokenizer(ABC):
return return
def countStats(self) -> dict[str, int]: def buildOutline(self) -> None:
"""Build an outline of the text up to level 3 headings."""
tHandle = self._handle or ""
isNovel = self._isNovel
for tType, nHead, tText, _, _ in self._tokens:
if tType == self.T_TITLE:
prefix = "TT"
elif tType == self.T_HEAD1:
prefix = "PT" if isNovel else "H1"
elif tType == self.T_HEAD2:
prefix = "CH" if isNovel else "H2"
elif tType == self.T_HEAD3:
prefix = "SC" if isNovel else "H3"
else:
continue
key = f"{tHandle}:T{nHead:04d}"
text = tText.replace(nwHeadFmt.BR, " ").replace("&amp;", "&")
self._outline[key] = f"{prefix}|{text}"
return
def countStats(self) -> None:
"""Count stats on the tokenized text.""" """Count stats on the tokenized text."""
titleCount = self._counts.get("titleCount", 0) titleCount = self._counts.get("titleCount", 0)
paragraphCount = self._counts.get("paragraphCount", 0) paragraphCount = self._counts.get("paragraphCount", 0)
@@ -910,7 +932,7 @@ class Tokenizer(ABC):
self._counts["textWordChars"] = textWordChars self._counts["textWordChars"] = textWordChars
self._counts["titleWordChars"] = titleWordChars self._counts["titleWordChars"] = titleWordChars
return {} return
def saveRawMarkdown(self, path: str | Path) -> None: def saveRawMarkdown(self, path: str | Path) -> None:
"""Save the raw text to a plain text file.""" """Save the raw text to a plain text file."""
+2 -2
View File
@@ -28,7 +28,7 @@ import logging
from typing import TYPE_CHECKING, Literal, overload from typing import TYPE_CHECKING, Literal, overload
from pathlib import Path from pathlib import Path
from collections.abc import Generator, Iterator from collections.abc import Iterable, Iterator
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException from novelwriter.error import logException
@@ -387,7 +387,7 @@ class NWTree:
rootClasses.add(nwItem.itemClass) rootClasses.add(nwItem.itemClass)
return rootClasses return rootClasses
def iterRoots(self, itemClass: nwItemClass | None) -> Generator[tuple[str, NWItem]]: def iterRoots(self, itemClass: nwItemClass | None) -> Iterable[tuple[str, NWItem]]:
"""Iterate over all root items of a given class in order.""" """Iterate over all root items of a given class in order."""
for tHandle in self._order: for tHandle in self._order:
nwItem = self.__getitem__(tHandle) nwItem = self.__getitem__(tHandle)
+14 -2
View File
@@ -25,9 +25,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
from PyQt5.QtCore import Qt from PyQt5.QtCore import QSize, Qt
from PyQt5.QtGui import QWheelEvent from PyQt5.QtGui import QWheelEvent
from PyQt5.QtWidgets import QComboBox, QDoubleSpinBox, QSpinBox, QWidget from PyQt5.QtWidgets import QComboBox, QDoubleSpinBox, QSpinBox, QToolButton, QWidget
class NComboBox(QComboBox): class NComboBox(QComboBox):
@@ -85,3 +85,15 @@ class NDoubleSpinBox(QDoubleSpinBox):
return return
# END Class NDoubleSpinBox # END Class NDoubleSpinBox
class NIconToolButton(QToolButton):
def __init__(self, parent: QWidget, iconSize: int) -> None:
super().__init__(parent=parent)
self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.setIconSize(QSize(iconSize, iconSize))
self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
return
# END Class NIconToolButton
+21 -46
View File
@@ -53,15 +53,17 @@ from PyQt5.QtWidgets import (
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
from novelwriter.common import minmax, transferCase from novelwriter.common import minmax, transferCase
from novelwriter.constants import nwKeyWords, nwShortcode, nwUnicode from novelwriter.constants import nwKeyWords, nwShortcode, nwUnicode
from novelwriter.tools.lipsum import GuiLipsum
from novelwriter.core.document import NWDocument from novelwriter.core.document import NWDocument
from novelwriter.text.counting import standardCounter from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
from novelwriter.extensions.eventfilters import WheelEventFilter
from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.dochighlight import BLOCK_META, BLOCK_TITLE from novelwriter.gui.dochighlight import BLOCK_META, BLOCK_TITLE
from novelwriter.gui.editordocument import GuiTextDocument from novelwriter.gui.editordocument import GuiTextDocument
from novelwriter.extensions.eventfilters import WheelEventFilter from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
from novelwriter.text.counting import standardCounter
from novelwriter.tools.lipsum import GuiLipsum
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
@@ -2805,10 +2807,8 @@ class GuiDocEditHeader(QWidget):
self._docHandle = None self._docHandle = None
self._docOutline: dict[int, str] = {} self._docOutline: dict[int, str] = {}
fPx = int(0.9*SHARED.theme.fontPixelSize) iPx = SHARED.theme.baseIconSize
mPx = CONFIG.pxInt(8) mPx = CONFIG.pxInt(4)
hSp = CONFIG.pxInt(6)
iconSize = QSize(fPx, fPx)
# Main Widget Settings # Main Widget Settings
self.setAutoFillBackground(True) self.setAutoFillBackground(True)
@@ -2820,7 +2820,7 @@ class GuiDocEditHeader(QWidget):
self.itemTitle.setContentsMargins(0, 0, 0, 0) self.itemTitle.setContentsMargins(0, 0, 0, 0)
self.itemTitle.setAutoFillBackground(True) self.itemTitle.setAutoFillBackground(True)
self.itemTitle.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop) self.itemTitle.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
self.itemTitle.setFixedHeight(fPx) self.itemTitle.setFixedHeight(iPx)
lblFont = self.itemTitle.font() lblFont = self.itemTitle.font()
lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
@@ -2830,63 +2830,44 @@ class GuiDocEditHeader(QWidget):
self.outlineMenu = QMenu(self) self.outlineMenu = QMenu(self)
# Buttons # Buttons
self.tbButton = QToolButton(self) self.tbButton = NIconToolButton(self, iPx)
self.tbButton.setContentsMargins(0, 0, 0, 0)
self.tbButton.setIconSize(iconSize)
self.tbButton.setFixedSize(fPx, fPx)
self.tbButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.tbButton.setVisible(False) self.tbButton.setVisible(False)
self.tbButton.setToolTip(self.tr("Toggle Tool Bar")) self.tbButton.setToolTip(self.tr("Toggle Tool Bar"))
self.tbButton.clicked.connect(lambda: self.toggleToolBarRequest.emit()) self.tbButton.clicked.connect(lambda: self.toggleToolBarRequest.emit())
self.outlineButton = QToolButton(self) self.outlineButton = NIconToolButton(self, iPx)
self.outlineButton.setContentsMargins(0, 0, 0, 0)
self.outlineButton.setIconSize(iconSize)
self.outlineButton.setFixedSize(fPx, fPx)
self.outlineButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.outlineButton.setVisible(False) self.outlineButton.setVisible(False)
self.outlineButton.setToolTip(self.tr("Outline")) self.outlineButton.setToolTip(self.tr("Outline"))
self.outlineButton.setMenu(self.outlineMenu) self.outlineButton.setMenu(self.outlineMenu)
self.outlineButton.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
self.searchButton = QToolButton(self) self.searchButton = NIconToolButton(self, iPx)
self.searchButton.setContentsMargins(0, 0, 0, 0)
self.searchButton.setIconSize(iconSize)
self.searchButton.setFixedSize(fPx, fPx)
self.searchButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.searchButton.setVisible(False) self.searchButton.setVisible(False)
self.searchButton.setToolTip(self.tr("Search")) self.searchButton.setToolTip(self.tr("Search"))
self.searchButton.clicked.connect(self.docEditor.toggleSearch) self.searchButton.clicked.connect(self.docEditor.toggleSearch)
self.minmaxButton = QToolButton(self) self.minmaxButton = NIconToolButton(self, iPx)
self.minmaxButton.setContentsMargins(0, 0, 0, 0)
self.minmaxButton.setIconSize(iconSize)
self.minmaxButton.setFixedSize(fPx, fPx)
self.minmaxButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.minmaxButton.setVisible(False) self.minmaxButton.setVisible(False)
self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode")) self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode"))
self.minmaxButton.clicked.connect(lambda: self.docEditor.toggleFocusModeRequest.emit()) self.minmaxButton.clicked.connect(lambda: self.docEditor.toggleFocusModeRequest.emit())
self.closeButton = QToolButton(self) self.closeButton = NIconToolButton(self, iPx)
self.closeButton.setContentsMargins(0, 0, 0, 0)
self.closeButton.setIconSize(iconSize)
self.closeButton.setFixedSize(fPx, fPx)
self.closeButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.closeButton.setVisible(False) self.closeButton.setVisible(False)
self.closeButton.setToolTip(self.tr("Close")) self.closeButton.setToolTip(self.tr("Close"))
self.closeButton.clicked.connect(self._closeDocument) self.closeButton.clicked.connect(self._closeDocument)
# Assemble Layout # Assemble Layout
self.outerBox = QHBoxLayout() self.outerBox = QHBoxLayout()
self.outerBox.setSpacing(hSp)
self.outerBox.addWidget(self.tbButton, 0) self.outerBox.addWidget(self.tbButton, 0)
self.outerBox.addWidget(self.outlineButton, 0) self.outerBox.addWidget(self.outlineButton, 0)
self.outerBox.addWidget(self.searchButton, 0) self.outerBox.addWidget(self.searchButton, 0)
self.outerBox.addSpacing(mPx)
self.outerBox.addWidget(self.itemTitle, 1) self.outerBox.addWidget(self.itemTitle, 1)
self.outerBox.addSpacing(fPx + hSp) self.outerBox.addSpacing(mPx)
self.outerBox.addSpacing(iPx)
self.outerBox.addWidget(self.minmaxButton, 0) self.outerBox.addWidget(self.minmaxButton, 0)
self.outerBox.addWidget(self.closeButton, 0) self.outerBox.addWidget(self.closeButton, 0)
self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx) self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
self.outerBox.setSpacing(0)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
@@ -2896,7 +2877,7 @@ class GuiDocEditHeader(QWidget):
# Fix Margins and Size # Fix Margins and Size
# This is needed for high DPI systems. See issue #499. # This is needed for high DPI systems. See issue #499.
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
self.setMinimumHeight(fPx + 2*mPx) self.setMinimumHeight(iPx + 2*mPx)
self.updateTheme() self.updateTheme()
@@ -2944,15 +2925,9 @@ class GuiDocEditHeader(QWidget):
self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise")) self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
self.closeButton.setIcon(SHARED.theme.getIcon("close")) self.closeButton.setIcon(SHARED.theme.getIcon("close"))
colText = SHARED.theme.colText buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
buttonStyle = (
"QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
).format(colText.red(), colText.green(), colText.blue())
buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
self.tbButton.setStyleSheet(buttonStyle) self.tbButton.setStyleSheet(buttonStyle)
self.outlineButton.setStyleSheet(buttonStyleMenu) self.outlineButton.setStyleSheet(buttonStyle)
self.searchButton.setStyleSheet(buttonStyle) self.searchButton.setStyleSheet(buttonStyle)
self.minmaxButton.setStyleSheet(buttonStyle) self.minmaxButton.setStyleSheet(buttonStyle)
self.closeButton.setStyleSheet(buttonStyle) self.closeButton.setStyleSheet(buttonStyle)
+36 -64
View File
@@ -41,11 +41,13 @@ from PyQt5.QtWidgets import (
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwItemType, nwDocAction, nwDocMode
from novelwriter.error import logException
from novelwriter.constants import nwHeaders, nwUnicode from novelwriter.constants import nwHeaders, nwUnicode
from novelwriter.core.tohtml import ToHtml from novelwriter.core.tohtml import ToHtml
from novelwriter.enum import nwItemType, nwDocAction, nwDocMode
from novelwriter.error import logException
from novelwriter.extensions.eventfilters import WheelEventFilter from novelwriter.extensions.eventfilters import WheelEventFilter
from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -195,7 +197,10 @@ class GuiDocViewer(QTextBrowser):
sPos = self.verticalScrollBar().value() sPos = self.verticalScrollBar().value()
aDoc = ToHtml(SHARED.project) aDoc = ToHtml(SHARED.project)
aDoc.setPreview(CONFIG.viewComments, CONFIG.viewSynopsis) aDoc.setPreview(True)
aDoc.setKeywords(True)
aDoc.setComments(CONFIG.viewComments)
aDoc.setSynopsis(CONFIG.viewSynopsis)
aDoc.setLinkHeadings(True) aDoc.setLinkHeadings(True)
# Be extra careful here to prevent crashes when first opening a # Be extra careful here to prevent crashes when first opening a
@@ -628,12 +633,10 @@ class GuiDocViewHeader(QWidget):
# Internal Variables # Internal Variables
self._docHandle = None self._docHandle = None
self._docOutline: dict[int, tuple[str, int]] = {} self._docOutline: dict[str, tuple[str, int]] = {}
fPx = int(0.9*SHARED.theme.fontPixelSize) iPx = SHARED.theme.baseIconSize
hSp = CONFIG.pxInt(6) mPx = CONFIG.pxInt(4)
mPx = CONFIG.pxInt(8)
iconSize = QSize(fPx, fPx)
# Main Widget Settings # Main Widget Settings
self.setAutoFillBackground(True) self.setAutoFillBackground(True)
@@ -646,7 +649,7 @@ class GuiDocViewHeader(QWidget):
self.itemTitle.setContentsMargins(0, 0, 0, 0) self.itemTitle.setContentsMargins(0, 0, 0, 0)
self.itemTitle.setAutoFillBackground(True) self.itemTitle.setAutoFillBackground(True)
self.itemTitle.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop) self.itemTitle.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
self.itemTitle.setFixedHeight(fPx) self.itemTitle.setFixedHeight(iPx)
lblFont = self.itemTitle.font() lblFont = self.itemTitle.font()
lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
@@ -656,62 +659,43 @@ class GuiDocViewHeader(QWidget):
self.outlineMenu = QMenu(self) self.outlineMenu = QMenu(self)
# Buttons # Buttons
self.outlineButton = QToolButton(self) self.outlineButton = NIconToolButton(self, iPx)
self.outlineButton.setContentsMargins(0, 0, 0, 0)
self.outlineButton.setIconSize(iconSize)
self.outlineButton.setFixedSize(fPx, fPx)
self.outlineButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.outlineButton.setVisible(False) self.outlineButton.setVisible(False)
self.outlineButton.setToolTip(self.tr("Outline")) self.outlineButton.setToolTip(self.tr("Outline"))
self.outlineButton.setMenu(self.outlineMenu) self.outlineButton.setMenu(self.outlineMenu)
self.outlineButton.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
self.backButton = QToolButton(self) self.backButton = NIconToolButton(self, iPx)
self.backButton.setContentsMargins(0, 0, 0, 0)
self.backButton.setIconSize(iconSize)
self.backButton.setFixedSize(fPx, fPx)
self.backButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.backButton.setVisible(False) self.backButton.setVisible(False)
self.backButton.setToolTip(self.tr("Go Backward")) self.backButton.setToolTip(self.tr("Go Backward"))
self.backButton.clicked.connect(self.docViewer.navBackward) self.backButton.clicked.connect(self.docViewer.navBackward)
self.forwardButton = QToolButton(self) self.forwardButton = NIconToolButton(self, iPx)
self.forwardButton.setContentsMargins(0, 0, 0, 0)
self.forwardButton.setIconSize(iconSize)
self.forwardButton.setFixedSize(fPx, fPx)
self.forwardButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.forwardButton.setVisible(False) self.forwardButton.setVisible(False)
self.forwardButton.setToolTip(self.tr("Go Forward")) self.forwardButton.setToolTip(self.tr("Go Forward"))
self.forwardButton.clicked.connect(self.docViewer.navForward) self.forwardButton.clicked.connect(self.docViewer.navForward)
self.refreshButton = QToolButton(self) self.refreshButton = NIconToolButton(self, iPx)
self.refreshButton.setContentsMargins(0, 0, 0, 0)
self.refreshButton.setIconSize(iconSize)
self.refreshButton.setFixedSize(fPx, fPx)
self.refreshButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.refreshButton.setVisible(False) self.refreshButton.setVisible(False)
self.refreshButton.setToolTip(self.tr("Reload")) self.refreshButton.setToolTip(self.tr("Reload"))
self.refreshButton.clicked.connect(self._refreshDocument) self.refreshButton.clicked.connect(self._refreshDocument)
self.closeButton = QToolButton(self) self.closeButton = NIconToolButton(self, iPx)
self.closeButton.setContentsMargins(0, 0, 0, 0)
self.closeButton.setIconSize(iconSize)
self.closeButton.setFixedSize(fPx, fPx)
self.closeButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.closeButton.setVisible(False) self.closeButton.setVisible(False)
self.closeButton.setToolTip(self.tr("Close")) self.closeButton.setToolTip(self.tr("Close"))
self.closeButton.clicked.connect(self._closeDocument) self.closeButton.clicked.connect(self._closeDocument)
# Assemble Layout # Assemble Layout
self.outerBox = QHBoxLayout() self.outerBox = QHBoxLayout()
self.outerBox.setSpacing(hSp)
self.outerBox.addWidget(self.outlineButton, 0) self.outerBox.addWidget(self.outlineButton, 0)
self.outerBox.addWidget(self.backButton, 0) self.outerBox.addWidget(self.backButton, 0)
self.outerBox.addWidget(self.forwardButton, 0) self.outerBox.addWidget(self.forwardButton, 0)
self.outerBox.addSpacing(mPx)
self.outerBox.addWidget(self.itemTitle, 1) self.outerBox.addWidget(self.itemTitle, 1)
self.outerBox.addSpacing(fPx + hSp) self.outerBox.addSpacing(mPx)
self.outerBox.addSpacing(iPx)
self.outerBox.addWidget(self.refreshButton, 0) self.outerBox.addWidget(self.refreshButton, 0)
self.outerBox.addWidget(self.closeButton, 0) self.outerBox.addWidget(self.closeButton, 0)
self.outerBox.setSpacing(0)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
@@ -719,7 +703,7 @@ class GuiDocViewHeader(QWidget):
# This is needed for high DPI systems. See issue #499. # This is needed for high DPI systems. See issue #499.
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx) self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
self.setMinimumHeight(fPx + 2*mPx) self.setMinimumHeight(iPx + 2*mPx)
# Fix the Colours # Fix the Colours
self.updateTheme() self.updateTheme()
@@ -746,9 +730,10 @@ class GuiDocViewHeader(QWidget):
self.refreshButton.setVisible(False) self.refreshButton.setVisible(False)
return return
def setOutline(self, data: dict[int, tuple[str, int]]) -> None: def setOutline(self, data: dict[str, tuple[str, int]]) -> None:
"""Set the document outline dataset.""" """Set the document outline dataset."""
if data != self._docOutline: tHandle = self._docHandle
if data != self._docOutline and tHandle:
self.outlineMenu.clear() self.outlineMenu.clear()
entries = [] entries = []
minLevel = 5 minLevel = 5
@@ -760,7 +745,7 @@ class GuiDocViewHeader(QWidget):
indent = " "*(level - minLevel) indent = " "*(level - minLevel)
action = self.outlineMenu.addAction(f"{indent}{text}") action = self.outlineMenu.addAction(f"{indent}{text}")
action.triggered.connect( action.triggered.connect(
lambda _, title=title: self.docViewer.navigateTo(f"#{title}") lambda _, title=title: self.docViewer.navigateTo(f"#{tHandle}:{title}")
) )
self._docOutline = data self._docOutline = data
return return
@@ -773,14 +758,8 @@ class GuiDocViewHeader(QWidget):
self.refreshButton.setIcon(SHARED.theme.getIcon("refresh")) self.refreshButton.setIcon(SHARED.theme.getIcon("refresh"))
self.closeButton.setIcon(SHARED.theme.getIcon("close")) self.closeButton.setIcon(SHARED.theme.getIcon("close"))
colText = SHARED.theme.colText buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
buttonStyle = ( self.outlineButton.setStyleSheet(buttonStyle)
"QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
).format(colText.red(), colText.green(), colText.blue())
buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
self.outlineButton.setStyleSheet(buttonStyleMenu)
self.backButton.setStyleSheet(buttonStyle) self.backButton.setStyleSheet(buttonStyle)
self.forwardButton.setStyleSheet(buttonStyle) self.forwardButton.setStyleSheet(buttonStyle)
self.refreshButton.setStyleSheet(buttonStyle) self.refreshButton.setStyleSheet(buttonStyle)
@@ -878,17 +857,16 @@ class GuiDocViewFooter(QWidget):
# Internal Variables # Internal Variables
self._docHandle = None self._docHandle = None
fPx = int(0.9*SHARED.theme.fontPixelSize) iPx = SHARED.theme.baseIconSize
hSp = CONFIG.pxInt(4) hSp = CONFIG.pxInt(4)
mPx = CONFIG.pxInt(4)
# Main Widget Settings # Main Widget Settings
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
self.setAutoFillBackground(True) self.setAutoFillBackground(True)
# Show/Hide Details # Show/Hide Details
self.showHide = QToolButton(self) self.showHide = NIconToolButton(self, iPx)
self.showHide.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.showHide.setIconSize(QSize(fPx, fPx))
self.showHide.clicked.connect(lambda: self.docViewer.togglePanelVisibility.emit()) self.showHide.clicked.connect(lambda: self.docViewer.togglePanelVisibility.emit())
self.showHide.setToolTip(self.tr("Show/Hide Viewer Panel")) self.showHide.setToolTip(self.tr("Show/Hide Viewer Panel"))
@@ -898,7 +876,7 @@ class GuiDocViewFooter(QWidget):
self.showComments.setCheckable(True) self.showComments.setCheckable(True)
self.showComments.setChecked(CONFIG.viewComments) self.showComments.setChecked(CONFIG.viewComments)
self.showComments.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) self.showComments.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
self.showComments.setIconSize(QSize(fPx, fPx)) self.showComments.setIconSize(QSize(iPx, iPx))
self.showComments.toggled.connect(self._doToggleComments) self.showComments.toggled.connect(self._doToggleComments)
self.showComments.setToolTip(self.tr("Show Comments")) self.showComments.setToolTip(self.tr("Show Comments"))
@@ -908,7 +886,7 @@ class GuiDocViewFooter(QWidget):
self.showSynopsis.setCheckable(True) self.showSynopsis.setCheckable(True)
self.showSynopsis.setChecked(CONFIG.viewSynopsis) self.showSynopsis.setChecked(CONFIG.viewSynopsis)
self.showSynopsis.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) self.showSynopsis.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
self.showSynopsis.setIconSize(QSize(fPx, fPx)) self.showSynopsis.setIconSize(QSize(iPx, iPx))
self.showSynopsis.toggled.connect(self._doToggleSynopsis) self.showSynopsis.toggled.connect(self._doToggleSynopsis)
self.showSynopsis.setToolTip(self.tr("Show Synopsis Comments")) self.showSynopsis.setToolTip(self.tr("Show Synopsis Comments"))
@@ -928,10 +906,9 @@ class GuiDocViewFooter(QWidget):
# Fix Margins and Size # Fix Margins and Size
# This is needed for high DPI systems. See issue #499. # This is needed for high DPI systems. See issue #499.
cM = CONFIG.pxInt(8)
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
self.outerBox.setContentsMargins(cM, cM, cM, cM) self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
self.setMinimumHeight(fPx + 2*cM) self.setMinimumHeight(iPx + 2*mPx)
# Fix the Colours # Fix the Colours
self.updateTheme() self.updateTheme()
@@ -954,12 +931,7 @@ class GuiDocViewFooter(QWidget):
self.showComments.setIcon(bulletIcon) self.showComments.setIcon(bulletIcon)
self.showSynopsis.setIcon(bulletIcon) self.showSynopsis.setIcon(bulletIcon)
colText = SHARED.theme.colText buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
buttonStyle = (
"QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
).format(colText.red(), colText.green(), colText.blue())
self.showHide.setStyleSheet(buttonStyle) self.showHide.setStyleSheet(buttonStyle)
self.showComments.setStyleSheet(buttonStyle) self.showComments.setStyleSheet(buttonStyle)
self.showSynopsis.setStyleSheet(buttonStyle) self.showSynopsis.setStyleSheet(buttonStyle)
+4 -24
View File
@@ -34,10 +34,11 @@ from PyQt5.QtWidgets import (
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocMode, nwItemClass
from novelwriter.common import checkInt from novelwriter.common import checkInt
from novelwriter.constants import nwHeaders, nwLabels, nwLists, trConst from novelwriter.constants import nwHeaders, nwLabels, nwLists, trConst
from novelwriter.core.index import IndexHeading, IndexItem from novelwriter.core.index import IndexHeading, IndexItem
from novelwriter.enum import nwDocMode, nwItemClass
from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -99,35 +100,14 @@ class GuiDocViewerPanel(QWidget):
def updateTheme(self, updateTabs: bool = True) -> None: def updateTheme(self, updateTabs: bool = True) -> None:
"""Update theme elements.""" """Update theme elements."""
qPalette = self.palette()
mPx = CONFIG.pxInt(2)
vPx = CONFIG.pxInt(4)
hPx = CONFIG.pxInt(8)
hCol = qPalette.highlight().color()
fCol = qPalette.text().color()
buttonStyle = (
"QToolButton {{padding: {0}px; margin: 0 0 {1}px 0; border: none; "
"background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({2}, {3}, {4}, 0.2);}} "
"QToolButton::menu-indicator {{image: none;}} "
).format(mPx, mPx, fCol.red(), fCol.green(), fCol.blue())
self.optsButton.setIcon(SHARED.theme.getIcon("menu")) self.optsButton.setIcon(SHARED.theme.getIcon("menu"))
self.optsButton.setStyleSheet(buttonStyle) self.optsButton.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON))
self.mainTabs.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_FLAT_TABS))
styleSheet = (
"QTabWidget::pane {{border: 0;}} "
"QTabWidget QTabBar::tab {{border: 0; padding: {0}px {1}px;}} "
"QTabWidget QTabBar::tab:selected {{color: rgb({2}, {3}, {4});}} "
).format(vPx, hPx, hCol.red(), hCol.green(), hCol.blue())
self.mainTabs.setStyleSheet(styleSheet)
self.updateHandle(self._lastHandle) self.updateHandle(self._lastHandle)
if updateTabs: if updateTabs:
self.tabBackRefs.updateTheme() self.tabBackRefs.updateTheme()
for tab in self.kwTabs.values(): for tab in self.kwTabs.values():
tab.updateTheme() tab.updateTheme()
return return
def openProjectTasks(self) -> None: def openProjectTasks(self) -> None:
+2 -2
View File
@@ -23,10 +23,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
from collections.abc import Generator
import logging import logging
from time import time from time import time
from collections.abc import Iterable
from PyQt5.QtGui import QTextBlock, QTextCursor, QTextDocument from PyQt5.QtGui import QTextBlock, QTextCursor, QTextDocument
from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtCore import QObject, pyqtSlot
@@ -114,7 +114,7 @@ class GuiTextDocument(QTextDocument):
return word, cPos, cLen, SHARED.spelling.suggestWords(word) return word, cPos, cLen, SHARED.spelling.suggestWords(word)
return "", -1, -1, [] return "", -1, -1, []
def iterBlockByType(self, cType: int, maxCount: int = 1000) -> Generator[QTextBlock]: def iterBlockByType(self, cType: int, maxCount: int = 1000) -> Iterable[QTextBlock]:
"""Iterate over all text blocks of a given type.""" """Iterate over all text blocks of a given type."""
count = 0 count = 0
for i in range(self.blockCount()): for i in range(self.blockCount()):
+10 -18
View File
@@ -35,16 +35,18 @@ from PyQt5.QtGui import QFocusEvent, QFont, QMouseEvent, QPalette, QResizeEvent
from PyQt5.QtCore import QModelIndex, QPoint, Qt, QSize, pyqtSlot, pyqtSignal from PyQt5.QtCore import QModelIndex, QPoint, Qt, QSize, pyqtSlot, pyqtSignal
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView,
QInputDialog, QMenu, QSizePolicy, QToolButton, QToolTip, QTreeWidget, QInputDialog, QMenu, QSizePolicy, QToolTip, QTreeWidget, QTreeWidgetItem,
QTreeWidgetItem, QVBoxLayout, QWidget QVBoxLayout, QWidget
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocMode, nwItemClass, nwOutline
from novelwriter.common import minmax from novelwriter.common import minmax
from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst
from novelwriter.core.index import IndexHeading from novelwriter.core.index import IndexHeading
from novelwriter.enum import nwDocMode, nwItemClass, nwOutline
from novelwriter.extensions.modified import NIconToolButton
from novelwriter.extensions.novelselector import NovelSelector from novelwriter.extensions.novelselector import NovelSelector
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
@@ -215,15 +217,13 @@ class GuiNovelToolBar(QWidget):
self.novelValue.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.novelValue.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.novelValue.novelSelectionChanged.connect(self.setCurrentRoot) self.novelValue.novelSelectionChanged.connect(self.setCurrentRoot)
self.tbNovel = QToolButton(self) self.tbNovel = NIconToolButton(self, iPx)
self.tbNovel.setToolTip(self.tr("Novel Root")) self.tbNovel.setToolTip(self.tr("Novel Root"))
self.tbNovel.setIconSize(QSize(iPx, iPx))
self.tbNovel.clicked.connect(self.novelValue.showPopup) self.tbNovel.clicked.connect(self.novelValue.showPopup)
# Refresh Button # Refresh Button
self.tbRefresh = QToolButton(self) self.tbRefresh = NIconToolButton(self, iPx)
self.tbRefresh.setToolTip(self.tr("Refresh")) self.tbRefresh.setToolTip(self.tr("Refresh"))
self.tbRefresh.setIconSize(QSize(iPx, iPx))
self.tbRefresh.clicked.connect(self._refreshNovelTree) self.tbRefresh.clicked.connect(self._refreshNovelTree)
# More Options Menu # More Options Menu
@@ -241,11 +241,9 @@ class GuiNovelToolBar(QWidget):
self.aLastColSize = self.mLastCol.addAction(self.tr("Column Size")) self.aLastColSize = self.mLastCol.addAction(self.tr("Column Size"))
self.aLastColSize.triggered.connect(self._selectLastColumnSize) self.aLastColSize.triggered.connect(self._selectLastColumnSize)
self.tbMore = QToolButton(self) self.tbMore = NIconToolButton(self, iPx)
self.tbMore.setToolTip(self.tr("More Options")) self.tbMore.setToolTip(self.tr("More Options"))
self.tbMore.setIconSize(QSize(iPx, iPx))
self.tbMore.setMenu(self.mMore) self.tbMore.setMenu(self.mMore)
self.tbMore.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
# Assemble # Assemble
self.outerBox = QHBoxLayout() self.outerBox = QHBoxLayout()
@@ -280,16 +278,10 @@ class GuiNovelToolBar(QWidget):
self.setPalette(qPalette) self.setPalette(qPalette)
# StyleSheets # StyleSheets
fadeCol = qPalette.text().color() buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
buttonStyle = (
"QToolButton {{padding: {0}px; border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
).format(CONFIG.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue())
buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
self.tbNovel.setStyleSheet(buttonStyle) self.tbNovel.setStyleSheet(buttonStyle)
self.tbRefresh.setStyleSheet(buttonStyle) self.tbRefresh.setStyleSheet(buttonStyle)
self.tbMore.setStyleSheet(buttonStyleMenu) self.tbMore.setStyleSheet(buttonStyle)
self.novelValue.setStyleSheet( self.novelValue.setStyleSheet(
"QComboBox {border-style: none; padding-left: 0;} " "QComboBox {border-style: none; padding-left: 0;} "
+15 -27
View File
@@ -38,20 +38,22 @@ from PyQt5.QtGui import (
from PyQt5.QtCore import QPoint, QTimer, Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtCore import QPoint, QTimer, Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView, QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView,
QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QLabel, QMenu, QShortcut, QSizePolicy, QTreeWidget, QTreeWidgetItem,
QTreeWidgetItem, QVBoxLayout, QWidget QVBoxLayout, QWidget
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout
from novelwriter.common import minmax from novelwriter.common import minmax
from novelwriter.constants import nwHeaders, nwUnicode, trConst, nwLabels from novelwriter.constants import nwHeaders, nwUnicode, trConst, nwLabels
from novelwriter.core.item import NWItem
from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter
from novelwriter.core.item import NWItem
from novelwriter.dialogs.docmerge import GuiDocMerge from novelwriter.dialogs.docmerge import GuiDocMerge
from novelwriter.dialogs.docsplit import GuiDocSplit from novelwriter.dialogs.docsplit import GuiDocSplit
from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.dialogs.projectsettings import GuiProjectSettings from novelwriter.dialogs.projectsettings import GuiProjectSettings
from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout
from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
@@ -275,22 +277,18 @@ class GuiProjectToolBar(QWidget):
# Quick Links # Quick Links
self.mQuick = QMenu(self) self.mQuick = QMenu(self)
self.tbQuick = QToolButton(self) self.tbQuick = NIconToolButton(self, iPx)
self.tbQuick.setToolTip("%s [Ctrl+L]" % self.tr("Quick Links")) self.tbQuick.setToolTip("%s [Ctrl+L]" % self.tr("Quick Links"))
self.tbQuick.setShortcut("Ctrl+L") self.tbQuick.setShortcut("Ctrl+L")
self.tbQuick.setIconSize(QSize(iPx, iPx))
self.tbQuick.setMenu(self.mQuick) self.tbQuick.setMenu(self.mQuick)
self.tbQuick.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
# Move Buttons # Move Buttons
self.tbMoveU = QToolButton(self) self.tbMoveU = NIconToolButton(self, iPx)
self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up")) self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up"))
self.tbMoveU.setIconSize(QSize(iPx, iPx))
self.tbMoveU.clicked.connect(lambda: self.projTree.moveTreeItem(-1)) self.tbMoveU.clicked.connect(lambda: self.projTree.moveTreeItem(-1))
self.tbMoveD = QToolButton(self) self.tbMoveD = NIconToolButton(self, iPx)
self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down")) self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down"))
self.tbMoveD.setIconSize(QSize(iPx, iPx))
self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1)) self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1))
# Add Item Menu # Add Item Menu
@@ -329,12 +327,10 @@ class GuiProjectToolBar(QWidget):
self.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"])) self.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"]))
self._buildRootMenu() self._buildRootMenu()
self.tbAdd = QToolButton(self) self.tbAdd = NIconToolButton(self, iPx)
self.tbAdd.setToolTip("%s [Ctrl+N]" % self.tr("Add Item")) self.tbAdd.setToolTip("%s [Ctrl+N]" % self.tr("Add Item"))
self.tbAdd.setShortcut("Ctrl+N") self.tbAdd.setShortcut("Ctrl+N")
self.tbAdd.setIconSize(QSize(iPx, iPx))
self.tbAdd.setMenu(self.mAdd) self.tbAdd.setMenu(self.mAdd)
self.tbAdd.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
# More Options Menu # More Options Menu
self.mMore = QMenu(self) self.mMore = QMenu(self)
@@ -348,11 +344,9 @@ class GuiProjectToolBar(QWidget):
self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash")) self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash"))
self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash()) self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash())
self.tbMore = QToolButton(self) self.tbMore = NIconToolButton(self, iPx)
self.tbMore.setToolTip(self.tr("More Options")) self.tbMore.setToolTip(self.tr("More Options"))
self.tbMore.setIconSize(QSize(iPx, iPx))
self.tbMore.setMenu(self.mMore) self.tbMore.setMenu(self.mMore)
self.tbMore.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
# Assemble # Assemble
self.outerBox = QHBoxLayout() self.outerBox = QHBoxLayout()
@@ -382,18 +376,12 @@ class GuiProjectToolBar(QWidget):
qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base()) qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
self.setPalette(qPalette) self.setPalette(qPalette)
fadeCol = qPalette.text().color() buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
buttonStyle = ( self.tbQuick.setStyleSheet(buttonStyle)
"QToolButton {{padding: {0}px; border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
).format(CONFIG.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue())
buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
self.tbQuick.setStyleSheet(buttonStyleMenu)
self.tbMoveU.setStyleSheet(buttonStyle) self.tbMoveU.setStyleSheet(buttonStyle)
self.tbMoveD.setStyleSheet(buttonStyle) self.tbMoveD.setStyleSheet(buttonStyle)
self.tbAdd.setStyleSheet(buttonStyleMenu) self.tbAdd.setStyleSheet(buttonStyle)
self.tbMore.setStyleSheet(buttonStyleMenu) self.tbMore.setStyleSheet(buttonStyle)
self.tbQuick.setIcon(SHARED.theme.getIcon("bookmark")) self.tbQuick.setIcon(SHARED.theme.getIcon("bookmark"))
self.tbMoveU.setIcon(SHARED.theme.getIcon("up")) self.tbMoveU.setIcon(SHARED.theme.getIcon("up"))
+13 -26
View File
@@ -28,12 +28,14 @@ import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from PyQt5.QtGui import QPalette from PyQt5.QtGui import QPalette
from PyQt5.QtCore import QEvent, QPoint, Qt, QSize, pyqtSignal from PyQt5.QtCore import QEvent, QPoint, pyqtSignal
from PyQt5.QtWidgets import QMenu, QToolButton, QVBoxLayout, QWidget from PyQt5.QtWidgets import QMenu, QVBoxLayout, QWidget
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwView from novelwriter.enum import nwView
from novelwriter.extensions.eventfilters import StatusTipFilter from novelwriter.extensions.eventfilters import StatusTipFilter
from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.theme import STYLES_BIG_TOOLBUTTON
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
@@ -53,46 +55,37 @@ class GuiSideBar(QWidget):
self.mainGui = mainGui self.mainGui = mainGui
iPx = CONFIG.pxInt(24) iPx = CONFIG.pxInt(24)
iconSize = QSize(iPx, iPx)
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
self.installEventFilter(StatusTipFilter(mainGui)) self.installEventFilter(StatusTipFilter(mainGui))
# Buttons # Buttons
self.tbProject = QToolButton(self) self.tbProject = NIconToolButton(self, iPx)
self.tbProject.setToolTip("{0} [Ctrl+T]".format(self.tr("Project Tree View"))) self.tbProject.setToolTip("{0} [Ctrl+T]".format(self.tr("Project Tree View")))
self.tbProject.setIconSize(iconSize)
self.tbProject.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT)) self.tbProject.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT))
self.tbNovel = QToolButton(self) self.tbNovel = NIconToolButton(self, iPx)
self.tbNovel.setToolTip("{0} [Ctrl+T]".format(self.tr("Novel Tree View"))) self.tbNovel.setToolTip("{0} [Ctrl+T]".format(self.tr("Novel Tree View")))
self.tbNovel.setIconSize(iconSize)
self.tbNovel.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL)) self.tbNovel.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL))
self.tbOutline = QToolButton(self) self.tbOutline = NIconToolButton(self, iPx)
self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View"))) self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View")))
self.tbOutline.setIconSize(iconSize)
self.tbOutline.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE)) self.tbOutline.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE))
self.tbBuild = QToolButton(self) self.tbBuild = NIconToolButton(self, iPx)
self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript"))) self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
self.tbBuild.setIconSize(iconSize)
self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog) self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
self.tbDetails = QToolButton(self) self.tbDetails = NIconToolButton(self, iPx)
self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details"))) self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details")))
self.tbDetails.setIconSize(iconSize)
self.tbDetails.clicked.connect(self.mainGui.showNovelDetailsDialog) self.tbDetails.clicked.connect(self.mainGui.showNovelDetailsDialog)
self.tbStats = QToolButton(self) self.tbStats = NIconToolButton(self, iPx)
self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics"))) self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics")))
self.tbStats.setIconSize(iconSize)
self.tbStats.clicked.connect(self.mainGui.showWritingStatsDialog) self.tbStats.clicked.connect(self.mainGui.showWritingStatsDialog)
# Settings Menu # Settings Menu
self.tbSettings = QToolButton(self) self.tbSettings = NIconToolButton(self, iPx)
self.tbSettings.setToolTip(self.tr("Settings")) self.tbSettings.setToolTip(self.tr("Settings"))
self.tbSettings.setIconSize(iconSize)
self.tbSettings.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.mSettings = _PopRightMenu(self.tbSettings) self.mSettings = _PopRightMenu(self.tbSettings)
self.mSettings.addAction(self.mainGui.mainMenu.aEditWordList) self.mSettings.addAction(self.mainGui.mainMenu.aEditWordList)
@@ -101,7 +94,6 @@ class GuiSideBar(QWidget):
self.mSettings.addAction(self.mainGui.mainMenu.aPreferences) self.mSettings.addAction(self.mainGui.mainMenu.aPreferences)
self.tbSettings.setMenu(self.mSettings) self.tbSettings.setMenu(self.mSettings)
self.tbSettings.setPopupMode(QToolButton.InstantPopup)
# Assemble # Assemble
self.outerBox = QVBoxLayout() self.outerBox = QVBoxLayout()
@@ -129,12 +121,7 @@ class GuiSideBar(QWidget):
qPalette.setBrush(QPalette.Window, qPalette.base()) qPalette.setBrush(QPalette.Window, qPalette.base())
self.setPalette(qPalette) self.setPalette(qPalette)
fadeCol = qPalette.text().color() buttonStyle = SHARED.theme.getStyleSheet(STYLES_BIG_TOOLBUTTON)
buttonStyle = (
"QToolButton {{padding: {0}px; border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
).format(CONFIG.pxInt(6), fadeCol.red(), fadeCol.green(), fadeCol.blue())
buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
self.tbProject.setIcon(SHARED.theme.getIcon("view_editor")) self.tbProject.setIcon(SHARED.theme.getIcon("view_editor"))
self.tbProject.setStyleSheet(buttonStyle) self.tbProject.setStyleSheet(buttonStyle)
@@ -155,7 +142,7 @@ class GuiSideBar(QWidget):
self.tbStats.setStyleSheet(buttonStyle) self.tbStats.setStyleSheet(buttonStyle)
self.tbSettings.setIcon(SHARED.theme.getIcon("settings")) self.tbSettings.setIcon(SHARED.theme.getIcon("settings"))
self.tbSettings.setStyleSheet(buttonStyleMenu) self.tbSettings.setStyleSheet(buttonStyle)
return return
+47
View File
@@ -43,6 +43,10 @@ from novelwriter.constants import nwLabels
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
STYLES_FLAT_TABS = "flatTabWidget"
STYLES_MIN_TOOLBUTTON = "minimalToolButton"
STYLES_BIG_TOOLBUTTON = "bigToolButton"
# =============================================================================================== # # =============================================================================================== #
# Gui Theme Class # Gui Theme Class
@@ -120,6 +124,7 @@ class GuiTheme:
self._syntaxList: list[tuple[str, str]] = [] self._syntaxList: list[tuple[str, str]] = []
self._availThemes: dict[str, Path] = {} self._availThemes: dict[str, Path] = {}
self._availSyntax: dict[str, Path] = {} self._availSyntax: dict[str, Path] = {}
self._styleSheets: dict[str, str] = {}
self._listConf(self._availSyntax, CONFIG.assetPath("syntax")) self._listConf(self._availSyntax, CONFIG.assetPath("syntax"))
self._listConf(self._availThemes, CONFIG.assetPath("themes")) self._listConf(self._availThemes, CONFIG.assetPath("themes"))
@@ -272,6 +277,9 @@ class GuiTheme:
# Apply Styles # Apply Styles
qApp.setPalette(self._guiPalette) qApp.setPalette(self._guiPalette)
# Reset stylesheets so that they are regenerated
self._buildStyleSheets(self._guiPalette)
return True return True
def loadSyntax(self) -> bool: def loadSyntax(self) -> bool:
@@ -366,6 +374,10 @@ class GuiTheme:
return self._syntaxList return self._syntaxList
def getStyleSheet(self, name: str) -> str:
"""Load a standard style sheet."""
return self._styleSheets.get(name, "")
## ##
# Internal Functions # Internal Functions
## ##
@@ -412,6 +424,41 @@ class GuiTheme:
self._guiPalette.setColor(value, self._parseColour(parser, section, name)) self._guiPalette.setColor(value, self._parseColour(parser, section, name))
return return
def _buildStyleSheets(self, palette: QPalette) -> None:
"""Build default style sheets."""
self._styleSheets = {}
aPx = CONFIG.pxInt(2)
bPx = CONFIG.pxInt(4)
cPx = CONFIG.pxInt(6)
dPx = CONFIG.pxInt(8)
tCol = palette.text().color()
hCol = palette.highlight().color()
# Flat Tab Widget and Tab Bar:
self._styleSheets[STYLES_FLAT_TABS] = (
"QTabWidget::pane {{border: 0;}} "
"QTabWidget QTabBar::tab {{border: 0; padding: {0}px {1}px;}} "
"QTabWidget QTabBar::tab:selected {{color: rgb({2}, {3}, {4});}} "
).format(bPx, dPx, hCol.red(), hCol.green(), hCol.blue())
# Minimal Tool Button
self._styleSheets[STYLES_MIN_TOOLBUTTON] = (
"QToolButton {{padding: {0}px; margin: 0; border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({1}, {2}, {3}, 0.2);}} "
"QToolButton::menu-indicator {{image: none;}} "
).format(aPx, tCol.red(), tCol.green(), tCol.blue())
# Big Tool Button
self._styleSheets[STYLES_BIG_TOOLBUTTON] = (
"QToolButton {{padding: {0}px; margin: 0; border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({1}, {2}, {3}, 0.2);}} "
"QToolButton::menu-indicator {{image: none;}} "
).format(cPx, tCol.red(), tCol.green(), tCol.blue())
return
# End Class GuiTheme # End Class GuiTheme
+1 -1
View File
@@ -653,7 +653,7 @@ class GuiMain(QMainWindow):
self.docEditor.ensureCursorVisibleNoCentre() self.docEditor.ensureCursorVisibleNoCentre()
if sTitle: if sTitle:
self.docViewer.navigateTo(f"#{sTitle}") self.docViewer.navigateTo(f"#{tHandle}:{sTitle}")
return True return True
+126 -38
View File
@@ -31,25 +31,27 @@ from typing import TYPE_CHECKING
from datetime import datetime from datetime import datetime
from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent
from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot from PyQt5.QtCore import QSize, QTimer, QUrl, Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QDialog, QFormLayout, QGridLayout, QHBoxLayout, QLabel, QAbstractItemView, QDialog, QFormLayout, QGridLayout, QHBoxLayout, QLabel,
QListWidget, QListWidgetItem, QPushButton, QSizePolicy, QSplitter, QListWidget, QListWidgetItem, QPushButton, QSizePolicy, QSplitter,
QStackedWidget, QTextBrowser, QToolButton, QTreeWidget, QTreeWidgetItem, QStackedWidget, QTabWidget, QTextBrowser, QTreeWidget, QTreeWidgetItem,
QVBoxLayout, QWidget, qApp QVBoxLayout, QWidget, qApp
) )
from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.error import logException
from novelwriter.common import checkInt, fuzzyTime from novelwriter.common import checkInt, fuzzyTime
from novelwriter.core.tohtml import ToHtml
from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.tokenizer import HeadingFormatter
from novelwriter.core.buildsettings import BuildCollection, BuildSettings from novelwriter.core.buildsettings import BuildCollection, BuildSettings
from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.tohtml import ToHtml
from novelwriter.core.tokenizer import HeadingFormatter
from novelwriter.error import logException
from novelwriter.extensions.circularprogress import NProgressCircle
from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON
from novelwriter.tools.manusbuild import GuiManuscriptBuild from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.tools.manussettings import GuiBuildSettings from novelwriter.tools.manussettings import GuiBuildSettings
from novelwriter.extensions.circularprogress import NProgressCircle
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
@@ -101,29 +103,22 @@ class GuiManuscript(QDialog):
qPalette.setBrush(QPalette.Window, qPalette.base()) qPalette.setBrush(QPalette.Window, qPalette.base())
self.setPalette(qPalette) self.setPalette(qPalette)
fadeCol = qPalette.text().color() buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
buttonStyle = (
"QToolButton {{padding: {0}px; border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
).format(CONFIG.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue())
self.tbAdd = QToolButton(self) self.tbAdd = NIconToolButton(self, iPx)
self.tbAdd.setIcon(SHARED.theme.getIcon("add")) self.tbAdd.setIcon(SHARED.theme.getIcon("add"))
self.tbAdd.setIconSize(QSize(iPx, iPx))
self.tbAdd.setToolTip(self.tr("Add New Build")) self.tbAdd.setToolTip(self.tr("Add New Build"))
self.tbAdd.setStyleSheet(buttonStyle) self.tbAdd.setStyleSheet(buttonStyle)
self.tbAdd.clicked.connect(self._createNewBuild) self.tbAdd.clicked.connect(self._createNewBuild)
self.tbDel = QToolButton(self) self.tbDel = NIconToolButton(self, iPx)
self.tbDel.setIcon(SHARED.theme.getIcon("remove")) self.tbDel.setIcon(SHARED.theme.getIcon("remove"))
self.tbDel.setIconSize(QSize(iPx, iPx))
self.tbDel.setToolTip(self.tr("Delete Selected Build")) self.tbDel.setToolTip(self.tr("Delete Selected Build"))
self.tbDel.setStyleSheet(buttonStyle) self.tbDel.setStyleSheet(buttonStyle)
self.tbDel.clicked.connect(self._deleteSelectedBuild) self.tbDel.clicked.connect(self._deleteSelectedBuild)
self.tbEdit = QToolButton(self) self.tbEdit = NIconToolButton(self, iPx)
self.tbEdit.setIcon(SHARED.theme.getIcon("edit")) self.tbEdit.setIcon(SHARED.theme.getIcon("edit"))
self.tbEdit.setIconSize(QSize(iPx, iPx))
self.tbEdit.setToolTip(self.tr("Edit Selected Build")) self.tbEdit.setToolTip(self.tr("Edit Selected Build"))
self.tbEdit.setStyleSheet(buttonStyle) self.tbEdit.setStyleSheet(buttonStyle)
self.tbEdit.clicked.connect(self._editSelectedBuild) self.tbEdit.clicked.connect(self._editSelectedBuild)
@@ -141,21 +136,31 @@ class GuiManuscript(QDialog):
# Builds # Builds
# ====== # ======
self.buildList = QListWidget() self.buildList = QListWidget(self)
self.buildList.setIconSize(QSize(iPx, iPx)) self.buildList.setIconSize(QSize(iPx, iPx))
self.buildList.doubleClicked.connect(self._editSelectedBuild) self.buildList.doubleClicked.connect(self._editSelectedBuild)
self.buildList.currentItemChanged.connect(self._updateBuildDetails) self.buildList.currentItemChanged.connect(self._updateBuildDetails)
self.buildList.setSelectionMode(QAbstractItemView.SingleSelection) self.buildList.setSelectionMode(QAbstractItemView.SingleSelection)
self.buildList.setDragDropMode(QAbstractItemView.InternalMove) self.buildList.setDragDropMode(QAbstractItemView.InternalMove)
# Details Tabs
# ============
self.buildDetails = _DetailsWidget(self) self.buildDetails = _DetailsWidget(self)
self.buildDetails.setColumnWidth( self.buildDetails.setColumnWidth(
CONFIG.pxInt(pOptions.getInt("GuiManuscript", "detailsWidth", 100)), CONFIG.pxInt(pOptions.getInt("GuiManuscript", "detailsWidth", 100)),
) )
self.buildOutline = _OutlineWidget(self)
self.detailsTabs = QTabWidget(self)
self.detailsTabs.addTab(self.buildDetails, self.tr("Build"))
self.detailsTabs.addTab(self.buildOutline, self.tr("Outline"))
self.detailsTabs.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_FLAT_TABS))
self.buildSplit = QSplitter(Qt.Orientation.Vertical, self) self.buildSplit = QSplitter(Qt.Orientation.Vertical, self)
self.buildSplit.addWidget(self.buildList) self.buildSplit.addWidget(self.buildList)
self.buildSplit.addWidget(self.buildDetails) self.buildSplit.addWidget(self.detailsTabs)
self.buildSplit.setSizes([ self.buildSplit.setSizes([
CONFIG.pxInt(pOptions.getInt("GuiManuscript", "listHeight", 50)), CONFIG.pxInt(pOptions.getInt("GuiManuscript", "listHeight", 50)),
CONFIG.pxInt(pOptions.getInt("GuiManuscript", "detailsHeight", 50)), CONFIG.pxInt(pOptions.getInt("GuiManuscript", "detailsHeight", 50)),
@@ -223,6 +228,9 @@ class GuiManuscript(QDialog):
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
self.setSizeGripEnabled(True) self.setSizeGripEnabled(True)
# Signals
self.buildOutline.outlineEntryClicked.connect(self.docPreview.navigateTo)
logger.debug("Ready: GuiManuscript") logger.debug("Ready: GuiManuscript")
return return
@@ -352,6 +360,7 @@ class GuiManuscript(QDialog):
"uuid": build.buildID, "uuid": build.buildID,
"time": int(time()), "time": int(time()),
"stats": buildObj.textStats, "stats": buildObj.textStats,
"outline": buildObj.textOutline,
"styles": buildObj.getStyleSheet(), "styles": buildObj.getStyleSheet(),
"html": buildObj.fullHTML, "html": buildObj.fullHTML,
} }
@@ -408,6 +417,7 @@ class GuiManuscript(QDialog):
build.getBool("format.justifyText") build.getBool("format.justifyText")
) )
self.docStats.updateStats(data.get("stats", {})) self.docStats.updateStats(data.get("stats", {}))
self.buildOutline.updateOutline(data.get("outline", {}))
return return
def _getSelectedBuild(self) -> BuildSettings | None: def _getSelectedBuild(self) -> BuildSettings | None:
@@ -517,7 +527,7 @@ class _DetailsWidget(QWidget):
self.listView = QTreeWidget(self) self.listView = QTreeWidget(self)
self.listView.setHeaderLabels([self.tr("Setting"), self.tr("Value")]) self.listView.setHeaderLabels([self.tr("Setting"), self.tr("Value")])
self.listView.setIndentation(SHARED.theme.baseIconSize) self.listView.setIndentation(SHARED.theme.baseIconSize)
self.listView.setSelectionMode(QAbstractItemView.NoSelection) self.listView.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
# Assemble # Assemble
self.outerBox = QVBoxLayout() self.outerBox = QVBoxLayout()
@@ -649,6 +659,85 @@ class _DetailsWidget(QWidget):
# END Class _DetailsWidget # END Class _DetailsWidget
class _OutlineWidget(QWidget):
D_LINE = Qt.ItemDataRole.UserRole
outlineEntryClicked = pyqtSignal(str)
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
self._outline = {}
# Tree Widget
self.listView = QTreeWidget(self)
self.listView.setHeaderHidden(True)
self.listView.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.listView.itemClicked.connect(self._onItemClick)
# Assemble
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(self.listView)
self.outerBox.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.outerBox)
return
def updateOutline(self, data: dict[str, str]) -> None:
"""Update the outline."""
if isinstance(data, dict) and data != self._outline:
self.listView.clear()
tFont = self.font()
tFont.setBold(True)
tBrush = self.palette().highlight()
hFont = self.font()
hFont.setBold(True)
hFont.setUnderline(True)
root = self.listView.invisibleRootItem()
parent = root
indent = False
for anchor, entry in data.items():
prefix, _, text = entry.partition("|")
if prefix in ("TT", "PT", "CH", "SC", "H1", "H2"):
item = QTreeWidgetItem([text])
item.setData(0, self.D_LINE, anchor)
if prefix == "TT":
item.setFont(0, tFont)
item.setForeground(0, tBrush)
root.addChild(item)
parent = root
elif prefix == "PT":
item.setFont(0, hFont)
root.addChild(item)
parent = root
elif prefix in ("CH", "H1"):
root.addChild(item)
parent = item
elif prefix in ("SC", "H2"):
parent.addChild(item)
indent = True
self.listView.setIndentation(SHARED.theme.baseIconSize if indent else CONFIG.pxInt(4))
self._outline = data
return
##
# Private Slots
##
def _onItemClick(self, item: QTreeWidgetItem) -> None:
"""Process tree item click."""
self.outlineEntryClicked.emit(str(item.data(0, self.D_LINE)))
return
# END Class _OutlineWidget
class _PreviewWidget(QTextBrowser): class _PreviewWidget(QTextBrowser):
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
@@ -688,7 +777,7 @@ class _PreviewWidget(QTextBrowser):
self.ageLabel.setFont(aFont) self.ageLabel.setFont(aFont)
self.ageLabel.setPalette(aPalette) self.ageLabel.setPalette(aPalette)
self.ageLabel.setAutoFillBackground(True) self.ageLabel.setAutoFillBackground(True)
self.ageLabel.setAlignment(Qt.AlignCenter) self.ageLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.ageLabel.setFixedHeight(int(2.1*SHARED.theme.fontPixelSize)) self.ageLabel.setFixedHeight(int(2.1*SHARED.theme.fontPixelSize))
# Progress # Progress
@@ -706,7 +795,7 @@ class _PreviewWidget(QTextBrowser):
self._updateBuildAge() self._updateBuildAge()
# Age Timer # Age Timer
self.ageTimer = QTimer() self.ageTimer = QTimer(self)
self.ageTimer.setInterval(10) self.ageTimer.setInterval(10)
self.ageTimer.timeout.connect(self._updateBuildAge) self.ageTimer.timeout.connect(self._updateBuildAge)
self.ageTimer.start() self.ageTimer.start()
@@ -727,9 +816,9 @@ class _PreviewWidget(QTextBrowser):
"""Enable/disable the justify text option.""" """Enable/disable the justify text option."""
pOptions = self.document().defaultTextOption() pOptions = self.document().defaultTextOption()
if state: if state:
pOptions.setAlignment(Qt.AlignJustify) pOptions.setAlignment(Qt.AlignmentFlag.AlignJustify)
else: else:
pOptions.setAlignment(Qt.AlignAbsolute) pOptions.setAlignment(Qt.AlignmentFlag.AlignAbsolute)
self.document().setDefaultTextOption(pOptions) self.document().setDefaultTextOption(pOptions)
return return
@@ -765,7 +854,7 @@ class _PreviewWidget(QTextBrowser):
def setContent(self, data: dict) -> None: def setContent(self, data: dict) -> None:
"""Set the content of the preview widget.""" """Set the content of the preview widget."""
sPos = self.verticalScrollBar().value() sPos = self.verticalScrollBar().value()
qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
self.buildProgress.setCentreText(self.tr("Processing ...")) self.buildProgress.setCentreText(self.tr("Processing ..."))
qApp.processEvents() qApp.processEvents()
@@ -775,8 +864,6 @@ class _PreviewWidget(QTextBrowser):
html = "".join(data.get("html", [])) html = "".join(data.get("html", []))
html = html.replace("\t", "!!tab!!") html = html.replace("\t", "!!tab!!")
html = html.replace("<del>", "<span style='text-decoration: line-through;'>")
html = html.replace("</del>", "</span>")
self.setHtml(html) self.setHtml(html)
qApp.processEvents() qApp.processEvents()
while self.find("!!tab!!"): while self.find("!!tab!!"):
@@ -821,6 +908,13 @@ class _PreviewWidget(QTextBrowser):
qApp.restoreOverrideCursor() qApp.restoreOverrideCursor()
return return
@pyqtSlot(str)
def navigateTo(self, anchor: str) -> None:
"""Go to a specific #link in the document."""
logger.debug("Moving to anchor '#%s'", anchor)
self.setSource(QUrl(f"#{anchor}"))
return
## ##
# Private Slots # Private Slots
## ##
@@ -882,19 +976,13 @@ class _StatsWidget(QWidget):
self.minWidget = QWidget(self) self.minWidget = QWidget(self)
self.maxWidget = QWidget(self) self.maxWidget = QWidget(self)
fPx = int(0.6*SHARED.theme.fontPixelSize) iPx = int(0.6*SHARED.theme.baseIconSize)
toggleIcon = SHARED.theme.getToggleIcon("unfold", (fPx, fPx)) toggleIcon = SHARED.theme.getToggleIcon("unfold", (iPx, iPx))
fadeCol = self.palette().text().color() self.toggleButton = NIconToolButton(self, iPx)
buttonStyle = (
"QToolButton {{padding: 0; border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
).format(fadeCol.red(), fadeCol.green(), fadeCol.blue())
self.toggleButton = QToolButton(self)
self.toggleButton.setCheckable(True) self.toggleButton.setCheckable(True)
self.toggleButton.setIcon(toggleIcon) self.toggleButton.setIcon(toggleIcon)
self.toggleButton.setStyleSheet(buttonStyle) self.toggleButton.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON))
self.toggleButton.toggled.connect(self._toggleView) self.toggleButton.toggled.connect(self._toggleView)
self._buildMinimal() self._buildMinimal()
@@ -2,8 +2,8 @@
"meta": { "meta": {
"projectName": "Lorem Ipsum", "projectName": "Lorem Ipsum",
"novelAuthor": "lipsum.com", "novelAuthor": "lipsum.com",
"buildTime": 1706648873, "buildTime": 1711014280,
"buildTimeStr": "2024-01-30 22:07:53" "buildTimeStr": "2024-03-21 10:44:40"
}, },
"text": { "text": {
"nwd": [ "nwd": [
@@ -169,7 +169,7 @@
"Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo." "Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo."
], ],
[ [
"# Notes: Characters" "#! Notes: Characters"
], ],
[ [
"# Nobody Owens", "# Nobody Owens",
@@ -184,7 +184,7 @@
"Nullam scelerisque velit et tortor congue vestibulum a at nisi. Vivamus sodales ut turpis a convallis. In dignissim nibh at luctus sodales. Etiam sit amet rhoncus massa. Phasellus ligula magna, sollicitudin non imperdiet sit amet, volutpat vel magna. Nunc vestibulum tempor lectus, sit amet porta nunc hendrerit in. Curabitur non odio sit amet massa tincidunt facilisis. Integer et luctus nunc, eget euismod leo. Praesent faucibus metus sed purus convallis scelerisque. Fusce viverra lorem et placerat malesuada. In at elit malesuada, ullamcorper risus vitae, sodales dolor. Donec quis elementum lectus. Quisque eu eros at dui imperdiet euismod ut id neque." "Nullam scelerisque velit et tortor congue vestibulum a at nisi. Vivamus sodales ut turpis a convallis. In dignissim nibh at luctus sodales. Etiam sit amet rhoncus massa. Phasellus ligula magna, sollicitudin non imperdiet sit amet, volutpat vel magna. Nunc vestibulum tempor lectus, sit amet porta nunc hendrerit in. Curabitur non odio sit amet massa tincidunt facilisis. Integer et luctus nunc, eget euismod leo. Praesent faucibus metus sed purus convallis scelerisque. Fusce viverra lorem et placerat malesuada. In at elit malesuada, ullamcorper risus vitae, sodales dolor. Donec quis elementum lectus. Quisque eu eros at dui imperdiet euismod ut id neque."
], ],
[ [
"# Notes: Plot" "#! Notes: Plot"
], ],
[ [
"# Main Plot", "# Main Plot",
@@ -196,7 +196,7 @@
"Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur." "Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur."
], ],
[ [
"# Notes: World" "#! Notes: World"
], ],
[ [
"# Ancient Europe", "# Ancient Europe",
@@ -147,7 +147,7 @@ Pellentesque rhoncus pharetra eros, non mollis nisi pretium non. Mauris accumsan
Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo. Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo.
# Notes: Characters #! Notes: Characters
# Nobody Owens # Nobody Owens
@@ -160,7 +160,7 @@ Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque.
Nullam scelerisque velit et tortor congue vestibulum a at nisi. Vivamus sodales ut turpis a convallis. In dignissim nibh at luctus sodales. Etiam sit amet rhoncus massa. Phasellus ligula magna, sollicitudin non imperdiet sit amet, volutpat vel magna. Nunc vestibulum tempor lectus, sit amet porta nunc hendrerit in. Curabitur non odio sit amet massa tincidunt facilisis. Integer et luctus nunc, eget euismod leo. Praesent faucibus metus sed purus convallis scelerisque. Fusce viverra lorem et placerat malesuada. In at elit malesuada, ullamcorper risus vitae, sodales dolor. Donec quis elementum lectus. Quisque eu eros at dui imperdiet euismod ut id neque. Nullam scelerisque velit et tortor congue vestibulum a at nisi. Vivamus sodales ut turpis a convallis. In dignissim nibh at luctus sodales. Etiam sit amet rhoncus massa. Phasellus ligula magna, sollicitudin non imperdiet sit amet, volutpat vel magna. Nunc vestibulum tempor lectus, sit amet porta nunc hendrerit in. Curabitur non odio sit amet massa tincidunt facilisis. Integer et luctus nunc, eget euismod leo. Praesent faucibus metus sed purus convallis scelerisque. Fusce viverra lorem et placerat malesuada. In at elit malesuada, ullamcorper risus vitae, sodales dolor. Donec quis elementum lectus. Quisque eu eros at dui imperdiet euismod ut id neque.
# Notes: Plot #! Notes: Plot
# Main Plot # Main Plot
@@ -170,7 +170,7 @@ Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vit
Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur. Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur.
# Notes: World #! Notes: World
# Ancient Europe # Ancient Europe
+6 -2
View File
@@ -86,8 +86,12 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP
docBuild = NWBuildDocument(project, build) docBuild = NWBuildDocument(project, build)
docBuild.setCountEnabled(True) docBuild.setCountEnabled(True)
docBuild.setBuildOutline(True)
docBuild.queueAll() docBuild.queueAll()
assert docBuild._count is True
assert docBuild._outline is True
assert len(docBuild) == 21 assert len(docBuild) == 21
# Check FODT Build # Check FODT Build
@@ -575,10 +579,10 @@ def testCoreDocBuild_IterBuild(mockGUI, fncPath: Path, mockRnd):
"By Jane Doe\n\n" "By Jane Doe\n\n"
"## New Chapter\n\n\n" "## New Chapter\n\n\n"
"### New Scene\n\n\n" "### New Scene\n\n\n"
"# Notes: Plot\n\n" "#! Notes: Plot\n\n"
"# Main Plot\n" "# Main Plot\n"
"**Text**\n\n" "**Text**\n\n"
"# Notes: Characters\n\n" "#! Notes: Characters\n\n"
"# Jane Doe\n" "# Jane Doe\n"
"~~Text~~\n\n" "~~Text~~\n\n"
) )
+1
View File
@@ -1194,6 +1194,7 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
# Add a second novel # Add a second novel
mHandle = project.newRoot(nwItemClass.NOVEL) mHandle = project.newRoot(nwItemClass.NOVEL)
uHandle = project.newFile("Title Page", mHandle) uHandle = project.newFile("Title Page", mHandle)
assert uHandle is not None
itemIndex.add(uHandle, project.tree[uHandle]) # type: ignore itemIndex.add(uHandle, project.tree[uHandle]) # type: ignore
itemIndex.addItemHeading(uHandle, "T0001", "H1", "Novel 2") # type: ignore itemIndex.addItemHeading(uHandle, "T0001", "H1", "Novel 2") # type: ignore
assert uHandle in itemIndex assert uHandle in itemIndex
+28 -42
View File
@@ -38,7 +38,6 @@ def testCoreToHtml_ConvertHeaders(mockGUI):
# =================== # ===================
html._isNovel = True html._isNovel = True
html._isNote = False
html._isFirst = True html._isFirst = True
# Header 1 # Header 1
@@ -87,33 +86,33 @@ def testCoreToHtml_ConvertHeaders(mockGUI):
# ================== # ==================
html._isNovel = False html._isNovel = False
html._isNote = True
html._isFirst = True html._isFirst = True
html._handle = "0000000000000"
html.setLinkHeadings(True) html.setLinkHeadings(True)
# Header 1 # Header 1
html._text = "# Heading One\n" html._text = "# Heading One\n"
html.tokenizeText() html.tokenizeText()
html.doConvert() html.doConvert()
assert html.result == "<h1><a name='T0001'></a>Heading One</h1>\n" assert html.result == "<h1><a name='0000000000000:T0001'></a>Heading One</h1>\n"
# Header 2 # Header 2
html._text = "## Heading Two\n" html._text = "## Heading Two\n"
html.tokenizeText() html.tokenizeText()
html.doConvert() html.doConvert()
assert html.result == "<h2><a name='T0001'></a>Heading Two</h2>\n" assert html.result == "<h2><a name='0000000000000:T0001'></a>Heading Two</h2>\n"
# Header 3 # Header 3
html._text = "### Heading Three\n" html._text = "### Heading Three\n"
html.tokenizeText() html.tokenizeText()
html.doConvert() html.doConvert()
assert html.result == "<h3><a name='T0001'></a>Heading Three</h3>\n" assert html.result == "<h3><a name='0000000000000:T0001'></a>Heading Three</h3>\n"
# Header 4 # Header 4
html._text = "#### Heading Four\n" html._text = "#### Heading Four\n"
html.tokenizeText() html.tokenizeText()
html.doConvert() html.doConvert()
assert html.result == "<h4><a name='T0001'></a>Heading Four</h4>\n" assert html.result == "<h4><a name='0000000000000:T0001'></a>Heading Four</h4>\n"
# Title # Title
html._text = "#! Heading One\n" html._text = "#! Heading One\n"
@@ -121,14 +120,14 @@ def testCoreToHtml_ConvertHeaders(mockGUI):
html.doConvert() html.doConvert()
assert html.result == ( assert html.result == (
"<h1 class='title' style='text-align: center; page-break-before: always;'>" "<h1 class='title' style='text-align: center; page-break-before: always;'>"
"<a name='T0001'></a>Heading One</h1>\n" "<a name='0000000000000:T0001'></a>Heading One</h1>\n"
) )
# Unnumbered # Unnumbered
html._text = "##! Heading Two\n" html._text = "##! Heading Two\n"
html.tokenizeText() html.tokenizeText()
html.doConvert() html.doConvert()
assert html.result == "<h2><a name='T0001'></a>Heading Two</h2>\n" assert html.result == "<h2><a name='0000000000000:T0001'></a>Heading Two</h2>\n"
# END Test testCoreToHtml_ConvertHeaders # END Test testCoreToHtml_ConvertHeaders
@@ -140,7 +139,6 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
html = ToHtml(project) html = ToHtml(project)
html._isNovel = True html._isNovel = True
html._isNote = False
html._isFirst = True html._isFirst = True
# Paragraphs # Paragraphs
@@ -229,7 +227,6 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
# Multiple Keywords # Multiple Keywords
html._isFirst = False html._isFirst = False
html._noBreak = False
html.setKeywords(True) html.setKeywords(True)
html._text = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n" html._text = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n"
html.tokenizeText() html.tokenizeText()
@@ -247,7 +244,7 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
# Preview Mode # Preview Mode
# ============ # ============
html.setPreview(True, True) html.setPreview(True)
# Text (HTML4) # Text (HTML4)
html._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n" html._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
@@ -269,7 +266,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
html = ToHtml(project) html = ToHtml(project)
html._isNovel = True html._isNovel = True
html._isNote = False html._handle = "0000000000000"
html.setLinkHeadings(True) html.setLinkHeadings(True)
# Special Titles # Special Titles
@@ -277,24 +274,24 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Title # Title
html._tokens = [ html._tokens = [
(html.T_TITLE, 1, "A Title", None, html.A_PBB | html.A_CENTRE), (html.T_TITLE, 1, "A Title", [], html.A_PBB | html.A_CENTRE),
(html.T_EMPTY, 1, "", None, html.A_NONE), (html.T_EMPTY, 1, "", [], html.A_NONE),
] ]
html.doConvert() html.doConvert()
assert html.result == ( assert html.result == (
"<h1 class='title' style='text-align: center; page-break-before: always;'>" "<h1 class='title' style='text-align: center; page-break-before: always;'>"
"<a name='T0001'></a>A Title</h1>\n" "<a name='0000000000000:T0001'></a>A Title</h1>\n"
) )
# Unnumbered # Unnumbered
html._tokens = [ html._tokens = [
(html.T_HEAD2, 1, "Prologue", None, html.A_PBB), (html.T_HEAD2, 1, "Prologue", [], html.A_PBB),
(html.T_EMPTY, 1, "", None, html.A_NONE), (html.T_EMPTY, 1, "", [], html.A_NONE),
] ]
html.doConvert() html.doConvert()
assert html.result == ( assert html.result == (
"<h1 style='page-break-before: always;'>" "<h1 style='page-break-before: always;'>"
"<a name='T0001'></a>Prologue</h1>\n" "<a name='0000000000000:T0001'></a>Prologue</h1>\n"
) )
# Separators # Separators
@@ -302,16 +299,16 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Separator # Separator
html._tokens = [ html._tokens = [
(html.T_SEP, 1, "* * *", None, html.A_CENTRE), (html.T_SEP, 1, "* * *", [], html.A_CENTRE),
(html.T_EMPTY, 1, "", None, html.A_NONE), (html.T_EMPTY, 1, "", [], html.A_NONE),
] ]
html.doConvert() html.doConvert()
assert html.result == "<p class='sep' style='text-align: center;'>* * *</p>\n" assert html.result == "<p class='sep' style='text-align: center;'>* * *</p>\n"
# Skip # Skip
html._tokens = [ html._tokens = [
(html.T_SKIP, 1, "", None, html.A_NONE), (html.T_SKIP, 1, "", [], html.A_NONE),
(html.T_EMPTY, 1, "", None, html.A_NONE), (html.T_EMPTY, 1, "", [], html.A_NONE),
] ]
html.doConvert() html.doConvert()
assert html.result == "<p class='skip'>&nbsp;</p>\n" assert html.result == "<p class='skip'>&nbsp;</p>\n"
@@ -324,7 +321,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Align Left # Align Left
html.setStyles(False) html.setStyles(False)
html._tokens = [ html._tokens = [
(html.T_HEAD1, 1, "A Title", None, html.A_LEFT), (html.T_HEAD1, 1, "A Title", [], html.A_LEFT),
] ]
html.doConvert() html.doConvert()
assert html.result == ( assert html.result == (
@@ -335,7 +332,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Align Left # Align Left
html._tokens = [ html._tokens = [
(html.T_HEAD1, 1, "A Title", None, html.A_LEFT), (html.T_HEAD1, 1, "A Title", [], html.A_LEFT),
] ]
html.doConvert() html.doConvert()
assert html.result == ( assert html.result == (
@@ -344,7 +341,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Align Right # Align Right
html._tokens = [ html._tokens = [
(html.T_HEAD1, 1, "A Title", None, html.A_RIGHT), (html.T_HEAD1, 1, "A Title", [], html.A_RIGHT),
] ]
html.doConvert() html.doConvert()
assert html.result == ( assert html.result == (
@@ -353,7 +350,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Align Centre # Align Centre
html._tokens = [ html._tokens = [
(html.T_HEAD1, 1, "A Title", None, html.A_CENTRE), (html.T_HEAD1, 1, "A Title", [], html.A_CENTRE),
] ]
html.doConvert() html.doConvert()
assert html.result == ( assert html.result == (
@@ -362,7 +359,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Align Justify # Align Justify
html._tokens = [ html._tokens = [
(html.T_HEAD1, 1, "A Title", None, html.A_JUSTIFY), (html.T_HEAD1, 1, "A Title", [], html.A_JUSTIFY),
] ]
html.doConvert() html.doConvert()
assert html.result == ( assert html.result == (
@@ -374,7 +371,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Page Break Always # Page Break Always
html._tokens = [ html._tokens = [
(html.T_HEAD1, 1, "A Title", None, html.A_PBB | html.A_PBA), (html.T_HEAD1, 1, "A Title", [], html.A_PBB | html.A_PBA),
] ]
html.doConvert() html.doConvert()
assert html.result == ( assert html.result == (
@@ -388,7 +385,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Indent Left # Indent Left
html._tokens = [ html._tokens = [
(html.T_TEXT, 1, "Some text ...", [], html.A_IND_L), (html.T_TEXT, 1, "Some text ...", [], html.A_IND_L),
(html.T_EMPTY, 2, "", None, html.A_NONE), (html.T_EMPTY, 2, "", [], html.A_NONE),
] ]
html.doConvert() html.doConvert()
assert html.result == ( assert html.result == (
@@ -398,7 +395,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Indent Right # Indent Right
html._tokens = [ html._tokens = [
(html.T_TEXT, 1, "Some text ...", [], html.A_IND_R), (html.T_TEXT, 1, "Some text ...", [], html.A_IND_R),
(html.T_EMPTY, 2, "", None, html.A_NONE), (html.T_EMPTY, 2, "", [], html.A_NONE),
] ]
html.doConvert() html.doConvert()
assert html.result == ( assert html.result == (
@@ -461,7 +458,6 @@ def testCoreToHtml_SpecialCases(mockGUI):
) )
html._isFirst = False html._isFirst = False
html._noBreak = False
html._text = "## Heading <1>\n" html._text = "## Heading <1>\n"
html.tokenizeText() html.tokenizeText()
html.doConvert() html.doConvert()
@@ -606,16 +602,6 @@ def testCoreToHtml_Methods(mockGUI):
"<p>Text with &lt;brackets&gt; &amp; short&ndash;dash, long&mdash;dash &hellip;</p>\n" "<p>Text with &lt;brackets&gt; &amp; short&ndash;dash, long&mdash;dash &hellip;</p>\n"
) )
# With Preview
html.setPreview(True, True)
html._text = docText
html.doPreProcessing()
html.tokenizeText()
html.doConvert()
assert html.allMarkdown[-1] == (
"Text with <brackets> &amp; short&ndash;dash, long&mdash;dash &hellip;\n\n"
)
# Result Size # Result Size
assert html.getFullResultSize() == 147 assert html.getFullResultSize() == 147
@@ -669,7 +655,7 @@ def testCoreToHtml_Format(mockGUI):
# Preview Mode # Preview Mode
# ============ # ============
html.setPreview(True, True) html.setPreview(True)
assert html._formatSynopsis("synopsis text", True) == ( assert html._formatSynopsis("synopsis text", True) == (
"<p class='comment'><span class='synopsis'>Synopsis:</span> synopsis text</p>\n" "<p class='comment'><span class='synopsis'>Synopsis:</span> synopsis text</p>\n"
+89 -59
View File
@@ -33,7 +33,7 @@ from novelwriter.core.tokenizer import HeadingFormatter, Tokenizer, stripEscape
class BareTokenizer(Tokenizer): class BareTokenizer(Tokenizer):
def doConvert(self): def doConvert(self):
super().doConvert() super().doConvert() # type: ignore (deliberate check)
@pytest.mark.core @pytest.mark.core
@@ -68,7 +68,7 @@ def testCoreToken_Setters(mockGUI):
assert tokens._hideScene is False assert tokens._hideScene is False
assert tokens._hideHScene is False assert tokens._hideHScene is False
assert tokens._hideSection is False assert tokens._hideSection is False
assert tokens._linkHeaders is False assert tokens._linkHeadings is False
assert tokens._doBodyText is True assert tokens._doBodyText is True
assert tokens._doSynopsis is False assert tokens._doSynopsis is False
assert tokens._doComments is False assert tokens._doComments is False
@@ -124,7 +124,7 @@ def testCoreToken_Setters(mockGUI):
assert tokens._hideScene is True assert tokens._hideScene is True
assert tokens._hideHScene is True assert tokens._hideHScene is True
assert tokens._hideSection is True assert tokens._hideSection is True
assert tokens._linkHeaders is True assert tokens._linkHeadings is True
assert tokens._doBodyText is False assert tokens._doBodyText is False
assert tokens._doSynopsis is True assert tokens._doSynopsis is True
assert tokens._doComments is True assert tokens._doComments is True
@@ -181,34 +181,36 @@ def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncPath):
assert project.saveProject() assert project.saveProject()
# Root Heading # Root Heading
assert tokens.addRootHeading("stuff") is False assert len(tokens._tokens) == 0
assert tokens.addRootHeading(C.hSceneDoc) is False tokens.addRootHeading("stuff")
tokens.addRootHeading(C.hSceneDoc)
assert len(tokens._tokens) == 0
# First Page # First Page
assert tokens.addRootHeading(C.hPlotRoot) is True tokens.addRootHeading(C.hPlotRoot)
assert tokens.allMarkdown[-1] == "# Notes: Plot\n\n" assert tokens.allMarkdown[-1] == "#! Notes: Plot\n\n"
assert tokens._tokens[-1] == ( assert tokens._tokens[-1] == (
Tokenizer.T_TITLE, 0, "Notes: Plot", [], Tokenizer.A_CENTRE Tokenizer.T_TITLE, 1, "Notes: Plot", [], Tokenizer.A_CENTRE
) )
# Not First Page # Not First Page
assert tokens.addRootHeading(C.hPlotRoot) is True tokens.addRootHeading(C.hPlotRoot)
assert tokens.allMarkdown[-1] == "# Notes: Plot\n\n" assert tokens.allMarkdown[-1] == "#! Notes: Plot\n\n"
assert tokens._tokens[-1] == ( assert tokens._tokens[-1] == (
Tokenizer.T_TITLE, 0, "Notes: Plot", [], Tokenizer.A_CENTRE | Tokenizer.A_PBB Tokenizer.T_TITLE, 1, "Notes: Plot", [], Tokenizer.A_CENTRE | Tokenizer.A_PBB
) )
# Set Text # Set Text
assert tokens.setText("stuff") is False tokens.setText("stuff")
assert tokens.setText(C.hSceneDoc) is True assert tokens._text == ""
tokens.setText(C.hSceneDoc)
assert tokens._text == docText assert tokens._text == docText
assert tokens.setText(C.hSceneDoc, docText) is True tokens.setText(C.hSceneDoc, docText)
assert tokens._text == docText assert tokens._text == docText
assert tokens._isNone is False
assert tokens._isNovel is True assert tokens._isNovel is True
assert tokens._isNote is False
# Pre Processing # Pre Processing
tokens.doPreProcessing() tokens.doPreProcessing()
@@ -218,14 +220,14 @@ def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncPath):
savePath = fncPath / "dump.nwd" savePath = fncPath / "dump.nwd"
tokens.saveRawMarkdown(savePath) tokens.saveRawMarkdown(savePath)
assert readFile(savePath) == ( assert readFile(savePath) == (
"# Notes: Plot\n\n" "#! Notes: Plot\n\n"
"# Notes: Plot\n\n" "#! Notes: Plot\n\n"
) )
tokens.saveRawMarkdownJSON(savePath) tokens.saveRawMarkdownJSON(savePath)
assert json.loads(readFile(savePath))["text"] == { assert json.loads(readFile(savePath))["text"] == {
"nwd": [ "nwd": [
["# Notes: Plot"], ["#! Notes: Plot"],
["# Notes: Plot"] ["#! Notes: Plot"]
] ]
} }
@@ -259,7 +261,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File # Story File
tokens._isNovel = True tokens._isNovel = True
tokens._isNote = False
tokens._isFirst = True tokens._isFirst = True
tokens._text = "#! Novel Title\n" tokens._text = "#! Novel Title\n"
@@ -272,7 +273,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Note File # Note File
tokens._isNovel = False tokens._isNovel = False
tokens._isNote = True
tokens._isFirst = True tokens._isFirst = True
tokens._text = "#! Note Title\n" tokens._text = "#! Note Title\n"
@@ -288,7 +288,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File # Story File
tokens._isNovel = True tokens._isNovel = True
tokens._isNote = False
tokens._isFirst = True tokens._isFirst = True
tokens._text = "# Novel Title\n" tokens._text = "# Novel Title\n"
@@ -301,7 +300,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Note File # Note File
tokens._isNovel = False tokens._isNovel = False
tokens._isNote = True
tokens._isFirst = True tokens._isFirst = True
tokens._text = "# Note Title\n" tokens._text = "# Note Title\n"
@@ -317,8 +315,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File # Story File
tokens._isNovel = True tokens._isNovel = True
tokens._isNote = False
tokens._noBreak = False
tokens._text = "## Chapter One\n" tokens._text = "## Chapter One\n"
tokens.tokenizeText() tokens.tokenizeText()
@@ -330,7 +326,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Note File # Note File
tokens._isNovel = False tokens._isNovel = False
tokens._isNote = True
tokens._text = "## Heading 2\n" tokens._text = "## Heading 2\n"
tokens.tokenizeText() tokens.tokenizeText()
@@ -345,7 +340,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File # Story File
tokens._isNovel = True tokens._isNovel = True
tokens._isNote = False
tokens._text = "### Scene One\n" tokens._text = "### Scene One\n"
tokens.tokenizeText() tokens.tokenizeText()
@@ -357,7 +351,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Note File # Note File
tokens._isNovel = False tokens._isNovel = False
tokens._isNote = True
tokens._text = "### Heading 3\n" tokens._text = "### Heading 3\n"
tokens.tokenizeText() tokens.tokenizeText()
@@ -372,7 +365,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File # Story File
tokens._isNovel = True tokens._isNovel = True
tokens._isNote = False
tokens._text = "#### A Section\n" tokens._text = "#### A Section\n"
tokens.tokenizeText() tokens.tokenizeText()
@@ -384,7 +376,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Note File # Note File
tokens._isNovel = False tokens._isNovel = False
tokens._isNote = True
tokens._text = "#### Heading 4\n" tokens._text = "#### Heading 4\n"
tokens.tokenizeText() tokens.tokenizeText()
@@ -399,7 +390,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File # Story File
tokens._isNovel = True tokens._isNovel = True
tokens._isNote = False
tokens._isFirst = False tokens._isFirst = False
tokens._text = "#! Title\n" tokens._text = "#! Title\n"
@@ -412,7 +402,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Note File # Note File
tokens._isNovel = False tokens._isNovel = False
tokens._isNote = True
tokens._isFirst = False tokens._isFirst = False
tokens._text = "#! Title\n" tokens._text = "#! Title\n"
@@ -428,7 +417,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File # Story File
tokens._isNovel = True tokens._isNovel = True
tokens._isNote = False
tokens._text = "##! Prologue\n" tokens._text = "##! Prologue\n"
tokens.tokenizeText() tokens.tokenizeText()
@@ -440,7 +428,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Note File # Note File
tokens._isNovel = False tokens._isNovel = False
tokens._isNote = True
tokens._text = "##! Prologue\n" tokens._text = "##! Prologue\n"
tokens.tokenizeText() tokens.tokenizeText()
@@ -462,7 +449,6 @@ def testCoreToken_HeaderStyle(mockGUI):
def processStyle(text: str, first: bool) -> int: def processStyle(text: str, first: bool) -> int:
tokens._text = text tokens._text = text
tokens._isFirst = first tokens._isFirst = first
tokens._noBreak = first
tokens.tokenizeText() tokens.tokenizeText()
return tokens._tokens[0][4] return tokens._tokens[0][4]
@@ -479,7 +465,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Novel Docs # Novel Docs
tokens._isNovel = True tokens._isNovel = True
tokens._isNote = False
# First Document is False # First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_NONE assert processStyle("# Title\n", False) == Tokenizer.A_NONE
@@ -499,7 +484,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Note Docs # Note Docs
tokens._isNovel = False tokens._isNovel = False
tokens._isNote = True
# First Document is False # First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_NONE assert processStyle("# Title\n", False) == Tokenizer.A_NONE
@@ -530,7 +514,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Novel Docs # Novel Docs
tokens._isNovel = True tokens._isNovel = True
tokens._isNote = False
# First Document is False # First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_CENTRE assert processStyle("# Title\n", False) == Tokenizer.A_CENTRE
@@ -550,7 +533,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Note Docs # Note Docs
tokens._isNovel = False tokens._isNovel = False
tokens._isNote = True
# First Document is False # First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_NONE assert processStyle("# Title\n", False) == Tokenizer.A_NONE
@@ -581,7 +563,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Novel Docs # Novel Docs
tokens._isNovel = True tokens._isNovel = True
tokens._isNote = False
# First Document is False # First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_PBB assert processStyle("# Title\n", False) == Tokenizer.A_PBB
@@ -601,7 +582,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Note Docs # Note Docs
tokens._isNovel = False tokens._isNovel = False
tokens._isNote = True
# First Document is False # First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_NONE assert processStyle("# Title\n", False) == Tokenizer.A_NONE
@@ -632,7 +612,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Novel Docs # Novel Docs
tokens._isNovel = True tokens._isNovel = True
tokens._isNote = False
# First Document is False # First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB assert processStyle("# Title\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
@@ -652,7 +631,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Note Docs # Note Docs
tokens._isNovel = False tokens._isNovel = False
tokens._isNote = True
# First Document is False # First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_NONE assert processStyle("# Title\n", False) == Tokenizer.A_NONE
@@ -673,7 +651,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Check Separation # Check Separation
# ================ # ================
tokens._isNovel = True tokens._isNovel = True
tokens._isNote = False
# Title Styles # Title Styles
tokens.setTitleStyle(True, True) tokens.setTitleStyle(True, True)
@@ -1310,8 +1287,6 @@ def testCoreToken_ProcessHeaders(mockGUI):
# Story Files # Story Files
## ##
tokens._isNone = False
tokens._isNote = False
tokens._isNovel = True tokens._isNovel = True
# Titles # Titles
@@ -1523,6 +1498,73 @@ def testCoreToken_ProcessHeaders(mockGUI):
# END Test testCoreToken_ProcessHeaders # END Test testCoreToken_ProcessHeaders
@pytest.mark.core
def testCoreToken_BuildOutline(mockGUI, ipsumText):
"""Test stats counter of the Tokenizer class."""
project = NWProject()
project.data.setLanguage("en")
project._loadProjectLocalisation()
tokens = BareTokenizer(project)
# Novel
tokens._isNovel = True
tokens._handle = "0000000000000"
tokens._text = (
"#! My Novel\n\n"
"# Part 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"
"#### Section\n\n"
"Text\n\n"
)
tokens.tokenizeText()
tokens.buildOutline()
# Note
tokens._isNovel = False
tokens._handle = "0000000000001"
tokens._text = (
"#! My Notes\n\n"
"# Header 1\n\n"
"Text\n\n"
"## Header 2\n\n"
"Text\n\n"
"### Header 3\n\n"
"Text\n\n"
"#### Header 4\n\n"
"Text\n\n"
)
tokens.tokenizeText()
tokens.buildOutline()
# Check Outline
assert tokens.textOutline == {
"0000000000000:T0001": "TT|My Novel",
"0000000000000:T0002": "PT|Part One",
"0000000000000:T0003": "CH|Chapter One",
"0000000000000:T0004": "SC|Scene One",
"0000000000000:T0005": "SC|Scene Two",
"0000000000000:T0006": "CH|Chapter Two",
"0000000000000:T0007": "SC|Scene Three",
"0000000000000:T0008": "SC|Scene Four",
"0000000000001:T0001": "TT|My Notes",
"0000000000001:T0002": "H1|Header 1",
"0000000000001:T0003": "H2|Header 2",
"0000000000001:T0004": "H3|Header 3",
}
# END Test testCoreToken_BuildOutline
@pytest.mark.core @pytest.mark.core
def testCoreToken_CountStats(mockGUI, ipsumText): def testCoreToken_CountStats(mockGUI, ipsumText):
"""Test stats counter of the Tokenizer class.""" """Test stats counter of the Tokenizer class."""
@@ -1530,8 +1572,6 @@ def testCoreToken_CountStats(mockGUI, ipsumText):
project.data.setLanguage("en") project.data.setLanguage("en")
project._loadProjectLocalisation() project._loadProjectLocalisation()
tokens = BareTokenizer(project) tokens = BareTokenizer(project)
tokens._isNone = False
tokens._isNote = False
tokens._isNovel = True tokens._isNovel = True
# Short Text # Short Text
@@ -1740,8 +1780,6 @@ def testCoreToken_SceneSeparators(mockGUI):
project.data.setLanguage("en") project.data.setLanguage("en")
project._loadProjectLocalisation() project._loadProjectLocalisation()
md = ToMarkdown(project) md = ToMarkdown(project)
md._isNone = False
md._isNote = False
md._isNovel = True md._isNovel = True
# Separator Handling, Titles # Separator Handling, Titles
@@ -1858,8 +1896,6 @@ def testCoreToken_SceneSeparators(mockGUI):
# Requires a fresh builder class # Requires a fresh builder class
md = ToMarkdown(project) md = ToMarkdown(project)
md.setExtendedMarkdown() md.setExtendedMarkdown()
md._isNone = False
md._isNote = False
md._isNovel = True md._isNovel = True
md._text = ( md._text = (
@@ -1919,8 +1955,6 @@ def testCoreToken_HeaderVisibility(mockGUI):
# Novel Files # Novel Files
# =========== # ===========
md._isNone = False
md._isNote = False
md._isNovel = True md._isNovel = True
# Show All # Show All
@@ -1973,8 +2007,6 @@ def testCoreToken_HeaderVisibility(mockGUI):
# Note Files # Note Files
# ========== # ==========
md._isNone = False
md._isNote = True
md._isNovel = False md._isNovel = False
# Hide All # Hide All
@@ -2015,8 +2047,6 @@ def testCoreToken_CounterHandling(mockGUI):
project.data.setLanguage("en") project.data.setLanguage("en")
project._loadProjectLocalisation() project._loadProjectLocalisation()
md = ToMarkdown(project) md = ToMarkdown(project)
md._isNone = False
md._isNote = False
md._isNovel = True md._isNovel = True
# Counter Handling, Novel Titles # Counter Handling, Novel Titles
+6 -9
View File
@@ -35,7 +35,6 @@ def testCoreToMarkdown_ConvertHeaders(mockGUI):
toMD = ToMarkdown(project) toMD = ToMarkdown(project)
toMD._isNovel = True toMD._isNovel = True
toMD._isNote = False
toMD._isFirst = True toMD._isFirst = True
# Header 1 # Header 1
@@ -84,7 +83,6 @@ def testCoreToMarkdown_ConvertParagraphs(mockGUI):
toMD = ToMarkdown(project) toMD = ToMarkdown(project)
toMD._isNovel = True toMD._isNovel = True
toMD._isNote = False
toMD._isFirst = True toMD._isFirst = True
# Text for Extended Markdown # Text for Extended Markdown
@@ -200,15 +198,14 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
toMD = ToMarkdown(project) toMD = ToMarkdown(project)
toMD._isNovel = True toMD._isNovel = True
toMD._isNote = False
# Special Titles # Special Titles
# ============== # ==============
# Title # Title
toMD._tokens = [ toMD._tokens = [
(toMD.T_TITLE, 1, "A Title", None, toMD.A_PBB | toMD.A_CENTRE), (toMD.T_TITLE, 1, "A Title", [], toMD.A_PBB | toMD.A_CENTRE),
(toMD.T_EMPTY, 1, "", None, toMD.A_NONE), (toMD.T_EMPTY, 1, "", [], toMD.A_NONE),
] ]
toMD.doConvert() toMD.doConvert()
assert toMD.result == "# A Title\n\n" assert toMD.result == "# A Title\n\n"
@@ -218,16 +215,16 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
# Separator # Separator
toMD._tokens = [ toMD._tokens = [
(toMD.T_SEP, 1, "* * *", None, toMD.A_CENTRE), (toMD.T_SEP, 1, "* * *", [], toMD.A_CENTRE),
(toMD.T_EMPTY, 1, "", None, toMD.A_NONE), (toMD.T_EMPTY, 1, "", [], toMD.A_NONE),
] ]
toMD.doConvert() toMD.doConvert()
assert toMD.result == "* * *\n\n" assert toMD.result == "* * *\n\n"
# Skip # Skip
toMD._tokens = [ toMD._tokens = [
(toMD.T_SKIP, 1, "", None, toMD.A_NONE), (toMD.T_SKIP, 1, "", [], toMD.A_NONE),
(toMD.T_EMPTY, 1, "", None, toMD.A_NONE), (toMD.T_EMPTY, 1, "", [], toMD.A_NONE),
] ]
toMD.doConvert() toMD.doConvert()
assert toMD.result == "\n\n" assert toMD.result == "\n\n"
+2 -2
View File
@@ -652,7 +652,7 @@ def testCoreToOdt_ConvertDirect(mockGUI):
doc = ToOdt(project, isFlat=True) doc = ToOdt(project, isFlat=True)
doc._tokens = [ doc._tokens = [
(doc.T_TEXT, 1, "This is a paragraph", [], doc.A_JUSTIFY), (doc.T_TEXT, 1, "This is a paragraph", [], doc.A_JUSTIFY),
(doc.T_EMPTY, 1, "", None, doc.A_NONE), (doc.T_EMPTY, 1, "", [], doc.A_NONE),
] ]
doc.initDocument() doc.initDocument()
doc.doConvert() doc.doConvert()
@@ -673,7 +673,7 @@ def testCoreToOdt_ConvertDirect(mockGUI):
doc = ToOdt(project, isFlat=True) doc = ToOdt(project, isFlat=True)
doc._tokens = [ doc._tokens = [
(doc.T_TEXT, 1, "This is a paragraph", [], doc.A_PBA), (doc.T_TEXT, 1, "This is a paragraph", [], doc.A_PBA),
(doc.T_EMPTY, 1, "", None, doc.A_NONE), (doc.T_EMPTY, 1, "", [], doc.A_NONE),
] ]
doc.initDocument() doc.initDocument()
doc.doConvert() doc.doConvert()
+65 -6
View File
@@ -26,11 +26,12 @@ import pytest
from pathlib import Path from pathlib import Path
from pytestqt.qtbot import QtBot from pytestqt.qtbot import QtBot
from novelwriter.constants import nwHeadFmt
from tools import C, buildTestProject from tools import C, buildTestProject
from mocked import causeOSError from mocked import causeOSError
from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtWidgets import QAction, QDialogButtonBox from PyQt5.QtWidgets import QAction, QDialogButtonBox, QListWidgetItem
from PyQt5.QtPrintSupport import QPrintPreviewDialog from PyQt5.QtPrintSupport import QPrintPreviewDialog
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
@@ -152,11 +153,31 @@ def testManuscript_Builds(qtbot: QtBot, nwGUI: GuiMain, projPath: Path):
@pytest.mark.gui @pytest.mark.gui
def testManuscript_Features(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath: Path): def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
"""Test other features of the GuiManuscript dialog.""" """Test other features of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath) nwGUI.openProject(projPath)
nwGUI.openDocument(C.hTitlePage)
nwGUI.docEditor.setPlainText(
"#! My Novel\n\n"
"# Part 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"
"#### Section\n\n"
"Text\n\n"
)
nwGUI.saveDocument()
# qtbot.stop()
manus = GuiManuscript(nwGUI) manus = GuiManuscript(nwGUI)
manus.show() manus.show()
manus.loadContent() manus.loadContent()
@@ -185,6 +206,14 @@ def testManuscript_Features(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath:
manus.btnPreview.click() manus.btnPreview.click()
assert cacheFile.exists() is False assert cacheFile.exists() is False
first = manus.buildList.item(0)
assert isinstance(first, QListWidgetItem)
build = manus._builds.getBuild(first.data(GuiManuscript.D_KEY))
assert isinstance(build, BuildSettings)
build.setValue("headings.fmtScene", nwHeadFmt.TITLE)
build.setValue("headings.fmtHardScene", nwHeadFmt.TITLE)
manus._builds.setBuild(build)
# Preview again, and allow cache file to be created # Preview again, and allow cache file to be created
manus.buildList.setCurrentRow(0) manus.buildList.setCurrentRow(0)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged): with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
@@ -192,15 +221,45 @@ def testManuscript_Features(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath:
assert manus.docPreview.toPlainText().strip() != "" assert manus.docPreview.toPlainText().strip() != ""
assert cacheFile.exists() is True assert cacheFile.exists() is True
# Check Outline
assert manus.buildOutline._outline == {
"000000000000c:T0001": "TT|My Novel",
"000000000000c:T0002": "PT|Part One",
"000000000000c:T0003": "CH|Chapter One",
"000000000000c:T0004": "SC|Scene One",
"000000000000c:T0005": "SC|Scene Two",
"000000000000c:T0006": "CH|Chapter Two",
"000000000000c:T0007": "SC|Scene Three",
"000000000000c:T0008": "SC|Scene Four",
"000000000000e:T0001": "CH|New Chapter",
"000000000000f:T0001": "SC|New Scene",
}
listView = manus.buildOutline.listView
assert listView.topLevelItemCount() == 5
keyRole = manus.buildOutline.D_LINE
assert (item := listView.topLevelItem(0)) and item.data(0, keyRole) == "000000000000c:T0001"
assert (item := listView.topLevelItem(1)) and item.data(0, keyRole) == "000000000000c:T0002"
assert (item := listView.topLevelItem(2)) and item.data(0, keyRole) == "000000000000c:T0003"
assert (item := listView.topLevelItem(3)) and item.data(0, keyRole) == "000000000000c:T0006"
assert (item := listView.topLevelItem(4)) and item.data(0, keyRole) == "000000000000e:T0001"
# Click Outline
item = listView.topLevelItem(4)
assert item is not None
with qtbot.waitSignal(manus.buildOutline.outlineEntryClicked) as signal:
manus.buildOutline._onItemClick(item)
assert signal.args == ["000000000000e:T0001"]
# Check Preview Stats # Check Preview Stats
assert manus.docStats.mainStack.currentWidget() == manus.docStats.minWidget assert manus.docStats.mainStack.currentWidget() == manus.docStats.minWidget
assert manus.docStats.minWordCount.text() == "7" assert manus.docStats.minWordCount.text() == "25"
assert manus.docStats.minCharCount.text() == "31" assert manus.docStats.minCharCount.text() == "117"
manus.docStats.toggleButton.toggle() manus.docStats.toggleButton.toggle()
assert manus.docStats.mainStack.currentWidget() == manus.docStats.maxWidget assert manus.docStats.mainStack.currentWidget() == manus.docStats.maxWidget
assert manus.docStats.maxTotalWords.text() == "7" assert manus.docStats.maxTotalWords.text() == "25"
assert manus.docStats.maxTotalChars.text() == "31" assert manus.docStats.maxTotalChars.text() == "117"
# Toggle justify # Toggle justify
assert manus.docPreview.document().defaultTextOption().alignment() == Qt.AlignAbsolute assert manus.docPreview.document().defaultTextOption().alignment() == Qt.AlignAbsolute