diff --git a/novelwriter/formats/shared.py b/novelwriter/formats/shared.py
index 82a7c2c4..0c8fb701 100644
--- a/novelwriter/formats/shared.py
+++ b/novelwriter/formats/shared.py
@@ -29,6 +29,8 @@ import re
from enum import Flag, IntEnum
+from PyQt5.QtGui import QColor
+
ESCAPES = {r"\*": "*", r"\~": "~", r"\_": "_", r"\[": "[", r"\]": "]", r"\ ": ""}
RX_ESC = re.compile("|".join([re.escape(k) for k in ESCAPES.keys()]), flags=re.DOTALL)
@@ -40,6 +42,23 @@ def stripEscape(text: str) -> str:
return text
+class TextDocumentTheme:
+ """Default document theme."""
+
+ text: QColor = QColor(0, 0, 0)
+ highlight: QColor = QColor(255, 255, 166)
+ head: QColor = QColor(66, 113, 174)
+ comment: QColor = QColor(100, 100, 100)
+ note: QColor = QColor(129, 55, 9)
+ code: QColor = QColor(66, 113, 174)
+ modifier: QColor = QColor(129, 55, 9)
+ keyword: QColor = QColor(245, 135, 31)
+ tag: QColor = QColor(66, 113, 174)
+ optional: QColor = QColor(66, 113, 174)
+ dialog: QColor = QColor(66, 113, 174)
+ altdialog: QColor = QColor(129, 55, 9)
+
+
# Enums
# =====
diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py
index 1b5a0c80..e5b4c0a6 100644
--- a/novelwriter/formats/todocx.py
+++ b/novelwriter/formats/todocx.py
@@ -34,6 +34,7 @@ from typing import NamedTuple
from zipfile import ZIP_DEFLATED, ZipFile
from PyQt5.QtCore import QMarginsF, QSizeF
+from PyQt5.QtGui import QColor
from novelwriter import __version__
from novelwriter.common import firstFloat, xmlSubElem
@@ -41,6 +42,7 @@ from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwStyles
from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt
from novelwriter.formats.tokenizer import Tokenizer
+from novelwriter.types import QtHexRgb
logger = logging.getLogger(__name__)
@@ -80,6 +82,11 @@ def _mkTag(ns: str, tag: str) -> str:
return tag
+def _docXCol(color: QColor) -> str:
+ """Format a QColor as the DocX accepted value."""
+ return color.name(QtHexRgb).lstrip("#")
+
+
# Formatting Codes
X_BLD = 0x001 # Bold format
X_ITA = 0x002 # Italic format
@@ -115,8 +122,6 @@ S_HEAD = "Header"
S_FNOTE = "FootnoteText"
# Colours
-COL_HEAD_L12 = "2a6099"
-COL_HEAD_L34 = "444444"
COL_DIALOG_M = "2a6099"
COL_DIALOG_A = "813709"
COL_META_TXT = "813709"
@@ -535,7 +540,7 @@ class ToDocX(Tokenizer):
styles: list[DocXParStyle] = []
hScale = self._scaleHeads
- hColor = self._colorHeads
+ hColor = _docXCol(self._theme.head) if self._colorHeads else None
fSz = self._fontSize
fnSz = 0.8 * self._fontSize
fSz0 = (nwStyles.H_SIZES[0] * fSz) if hScale else fSz
@@ -582,7 +587,7 @@ class ToDocX(Tokenizer):
after=fSz * self._marginHead1[1],
line=fSz1 * self._lineHeight,
level=0,
- color=COL_HEAD_L12 if hColor else None,
+ color=hColor,
bold=self._boldHeads,
))
@@ -597,7 +602,7 @@ class ToDocX(Tokenizer):
after=fSz * self._marginHead2[1],
line=fSz2 * self._lineHeight,
level=1,
- color=COL_HEAD_L12 if hColor else None,
+ color=hColor,
bold=self._boldHeads,
))
@@ -612,7 +617,7 @@ class ToDocX(Tokenizer):
after=fSz * self._marginHead3[1],
line=fSz3 * self._lineHeight,
level=1,
- color=COL_HEAD_L34 if hColor else None,
+ color=hColor,
bold=self._boldHeads,
))
@@ -627,7 +632,7 @@ class ToDocX(Tokenizer):
after=fSz * self._marginHead4[1],
line=fSz4 * self._lineHeight,
level=1,
- color=COL_HEAD_L34 if hColor else None,
+ color=hColor,
bold=self._boldHeads,
))
diff --git a/novelwriter/formats/tohtml.py b/novelwriter/formats/tohtml.py
index 41449fc3..b00a95b8 100644
--- a/novelwriter/formats/tohtml.py
+++ b/novelwriter/formats/tohtml.py
@@ -34,7 +34,7 @@ from novelwriter.constants import nwHeadFmt, nwHtmlUnicode, nwKeyWords, nwLabels
from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape
from novelwriter.formats.tokenizer import Tokenizer
-from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
+from novelwriter.types import FONT_STYLE, FONT_WEIGHTS, QtHexRgb
logger = logging.getLogger(__name__)
@@ -354,16 +354,18 @@ class ToHtml(Tokenizer):
return []
mScale = self._lineHeight/1.15
+ tColor = self._theme.text.name(QtHexRgb)
+ hColor = self._theme.head.name(QtHexRgb) if self._colorHeads else tColor
styles = []
font = self._textFont
styles.append((
"body {{"
- "font-family: '{0:s}'; font-size: {1:d}pt; "
- "font-weight: {2:d}; font-style: {3:s};"
+ "color: {0:s}; font-family: '{1:s}'; font-size: {2:d}pt; "
+ "font-weight: {3:d}; font-style: {4:s};"
"}}"
).format(
- font.family(), font.pointSize(),
+ tColor, font.family(), font.pointSize(),
FONT_WEIGHTS.get(font.weight(), 400),
FONT_STYLE.get(font.style(), "normal"),
))
@@ -380,43 +382,43 @@ class ToHtml(Tokenizer):
))
styles.append((
"h1 {{"
- "color: rgb(66, 113, 174); "
+ "color: {0:s}; "
"page-break-after: avoid; "
- "margin-top: {0:.2f}em; "
- "margin-bottom: {1:.2f}em;"
+ "margin-top: {1:.2f}em; "
+ "margin-bottom: {2:.2f}em;"
"}}"
).format(
- mScale * self._marginHead1[0], mScale * self._marginHead1[1]
+ hColor, mScale * self._marginHead1[0], mScale * self._marginHead1[1]
))
styles.append((
"h2 {{"
- "color: rgb(66, 113, 174); "
+ "color: {0:s}; "
"page-break-after: avoid; "
- "margin-top: {0:.2f}em; "
- "margin-bottom: {1:.2f}em;"
+ "margin-top: {1:.2f}em; "
+ "margin-bottom: {2:.2f}em;"
"}}"
).format(
- mScale * self._marginHead2[0], mScale * self._marginHead2[1]
+ hColor, mScale * self._marginHead2[0], mScale * self._marginHead2[1]
))
styles.append((
"h3 {{"
- "color: rgb(50, 50, 50); "
+ "color: {0:s}; "
"page-break-after: avoid; "
- "margin-top: {0:.2f}em; "
- "margin-bottom: {1:.2f}em;"
+ "margin-top: {1:.2f}em; "
+ "margin-bottom: {2:.2f}em;"
"}}"
).format(
- mScale * self._marginHead3[0], mScale * self._marginHead3[1]
+ hColor, mScale * self._marginHead3[0], mScale * self._marginHead3[1]
))
styles.append((
"h4 {{"
- "color: rgb(50, 50, 50); "
+ "color: {0:s}; "
"page-break-after: avoid; "
- "margin-top: {0:.2f}em; "
- "margin-bottom: {1:.2f}em;"
+ "margin-top: {1:.2f}em; "
+ "margin-bottom: {2:.2f}em;"
"}}"
).format(
- mScale * self._marginHead4[0], mScale * self._marginHead4[1]
+ hColor, mScale * self._marginHead4[0], mScale * self._marginHead4[1]
))
styles.append((
".title {{"
diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py
index 7dd0a1f8..74f271b5 100644
--- a/novelwriter/formats/tokenizer.py
+++ b/novelwriter/formats/tokenizer.py
@@ -44,7 +44,9 @@ from novelwriter.constants import (
from novelwriter.core.index import processComment
from novelwriter.core.project import NWProject
from novelwriter.enum import nwComment, nwItemLayout
-from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Block, T_Formats, T_Note, TextFmt
+from novelwriter.formats.shared import (
+ BlockFmt, BlockTyp, T_Block, T_Formats, T_Note, TextDocumentTheme, TextFmt
+)
from novelwriter.text.patterns import REGEX_PATTERNS
logger = logging.getLogger(__name__)
@@ -110,6 +112,9 @@ class Tokenizer(ABC):
self._keepBreaks = True # Keep line breaks in paragraphs
self._defaultAlign = "left" # The default text alignment
+ # Other Setting
+ self._theme = TextDocumentTheme()
+
# Margins
self._marginTitle = nwStyles.T_MARGIN["H0"]
self._marginHead1 = nwStyles.T_MARGIN["H1"]
@@ -218,6 +223,11 @@ class Tokenizer(ABC):
# Setters
##
+ def setTheme(self, theme: TextDocumentTheme) -> None:
+ """Set the document colour theme."""
+ self._theme = theme
+ return
+
def setPartitionFormat(self, hFormat: str, hide: bool = False) -> None:
"""Set the partition format pattern."""
self._fmtPart = hFormat.strip()
diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py
index d5bde589..9f680814 100644
--- a/novelwriter/formats/toodt.py
+++ b/novelwriter/formats/toodt.py
@@ -35,7 +35,7 @@ from hashlib import sha256
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile
-from PyQt5.QtGui import QFont
+from PyQt5.QtGui import QColor, QFont
from novelwriter import __version__
from novelwriter.common import xmlIndent, xmlSubElem
@@ -220,10 +220,6 @@ class ToOdt(Tokenizer):
self._mDocRight = "2.000cm"
# Colour
- self._colHead12 = None
- self._opaHead12 = None
- self._colHead34 = None
- self._opaHead34 = None
self._colDialogM = "#2a6099"
self._colDialogA = "#813709"
self._colMetaTx = "#813709"
@@ -318,12 +314,6 @@ class ToOdt(Tokenizer):
self._mLeftFoot = self._emToCm(self._marginFoot[0])
self._mBotFoot = self._emToCm(self._marginFoot[1])
- if self._colorHeads:
- self._colHead12 = "#2a6099"
- self._opaHead12 = "100%"
- self._colHead34 = "#444444"
- self._opaHead34 = "100%"
-
self._fLineHeight = f"{round(100 * self._lineHeight):d}%"
self._fBlockIndent = self._emToCm(self._blockIndent)
self._fTextIndent = self._emToCm(self._firstWidth)
@@ -894,6 +884,8 @@ class ToOdt(Tokenizer):
def _useableStyles(self) -> None:
"""Set the usable styles."""
+ hColor = self._theme.head if self._colorHeads else None
+
# Add Text Body Style
style = ODTParagraphStyle(S_TEXT)
style.setDisplayName("Text body")
@@ -931,8 +923,7 @@ class ToOdt(Tokenizer):
style.setFontFamily(self._fontFamily)
style.setFontSize(self._fSizeText)
style.setFontWeight(self._fontWeight)
- style.setColour(self._colMetaTx)
- style.setOpacity(self._opaMetaTx)
+ style.setColour(self._theme.note)
style.packXML(self._xStyl)
self._mainPara[style.name] = style
@@ -982,8 +973,7 @@ class ToOdt(Tokenizer):
style.setFontFamily(self._fontFamily)
style.setFontSize(self._fSizeHead1)
style.setFontWeight(self._headWeight)
- style.setColour(self._colHead12)
- style.setOpacity(self._opaHead12)
+ style.setColour(hColor)
style.packXML(self._xStyl)
self._mainPara[style.name] = style
@@ -1000,8 +990,7 @@ class ToOdt(Tokenizer):
style.setFontFamily(self._fontFamily)
style.setFontSize(self._fSizeHead2)
style.setFontWeight(self._headWeight)
- style.setColour(self._colHead12)
- style.setOpacity(self._opaHead12)
+ style.setColour(hColor)
style.packXML(self._xStyl)
self._mainPara[style.name] = style
@@ -1018,8 +1007,7 @@ class ToOdt(Tokenizer):
style.setFontFamily(self._fontFamily)
style.setFontSize(self._fSizeHead3)
style.setFontWeight(self._headWeight)
- style.setColour(self._colHead34)
- style.setOpacity(self._opaHead34)
+ style.setColour(hColor)
style.packXML(self._xStyl)
self._mainPara[style.name] = style
@@ -1036,8 +1024,7 @@ class ToOdt(Tokenizer):
style.setFontFamily(self._fontFamily)
style.setFontSize(self._fSizeHead4)
style.setFontWeight(self._headWeight)
- style.setColour(self._colHead34)
- style.setOpacity(self._opaHead34)
+ style.setColour(hColor)
style.packXML(self._xStyl)
self._mainPara[style.name] = style
@@ -1298,14 +1285,14 @@ class ODTParagraphStyle:
self._tAttr["font-weight"][1] = None
return
- def setColour(self, value: str | None) -> None:
+ def setColour(self, value: QColor | None) -> None:
"""Set text colour."""
- self._tAttr["color"][1] = value
- return
-
- def setOpacity(self, value: str | None) -> None:
- """Set text opacity."""
- self._tAttr["opacity"][1] = value
+ if isinstance(value, QColor):
+ self._tAttr["color"][1] = value.name(QColor.NameFormat.HexRgb)
+ self._tAttr["opacity"][1] = f"{int(100.0 * value.alphaF())}%"
+ else:
+ self._tAttr["color"][1] = None
+ self._tAttr["opacity"][1] = None
return
##
diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py
index 243059ed..1a48a562 100644
--- a/novelwriter/formats/toqdoc.py
+++ b/novelwriter/formats/toqdoc.py
@@ -29,7 +29,7 @@ from pathlib import Path
from PyQt5.QtCore import QMarginsF, QSizeF
from PyQt5.QtGui import (
- QColor, QFont, QFontMetricsF, QPageSize, QTextBlockFormat, QTextCharFormat,
+ QFont, QFontMetricsF, QPageSize, QTextBlockFormat, QTextCharFormat,
QTextCursor, QTextDocument
)
from PyQt5.QtPrintSupport import QPrinter
@@ -49,21 +49,6 @@ logger = logging.getLogger(__name__)
T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
-class TextDocumentTheme:
- text: QColor = QColor(0, 0, 0)
- highlight: QColor = QColor(255, 255, 166)
- head: QColor = QColor(66, 113, 174)
- comment: QColor = QColor(100, 100, 100)
- note: QColor = QColor(129, 55, 9)
- code: QColor = QColor(66, 113, 174)
- modifier: QColor = QColor(129, 55, 9)
- keyword: QColor = QColor(245, 135, 31)
- tag: QColor = QColor(66, 113, 174)
- optional: QColor = QColor(66, 113, 174)
- dialog: QColor = QColor(66, 113, 174)
- altdialog: QColor = QColor(129, 55, 9)
-
-
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
if cursor.position() > 0:
cursor.insertBlock(bFmt)
@@ -84,7 +69,6 @@ class ToQTextDocument(Tokenizer):
self._document.setUndoRedoEnabled(False)
self._document.setDocumentMargin(0)
- self._theme = TextDocumentTheme()
self._styles: dict[int, T_TextStyle] = {}
self._usedNotes: dict[str, int] = {}
@@ -110,11 +94,6 @@ class ToQTextDocument(Tokenizer):
# Setters
##
- def setTheme(self, theme: TextDocumentTheme) -> None:
- """Set the document colour theme."""
- self._theme = theme
- return
-
def setPageLayout(
self, width: float, height: float, top: float, bottom: float, left: float, right: float
) -> None:
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index c2a75479..5b296fa8 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -44,7 +44,8 @@ from novelwriter.error import logException
from novelwriter.extensions.configlayout import NColourLabel
from novelwriter.extensions.eventfilters import WheelEventFilter
from novelwriter.extensions.modified import NIconToolButton
-from novelwriter.formats.toqdoc import TextDocumentTheme, ToQTextDocument
+from novelwriter.formats.shared import TextDocumentTheme
+from novelwriter.formats.toqdoc import ToQTextDocument
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
from novelwriter.types import (
QtAlignCenterTop, QtKeepAnchor, QtMouseLeft, QtMoveAnchor,
diff --git a/novelwriter/types.py b/novelwriter/types.py
index ab684dee..e9db0876 100644
--- a/novelwriter/types.py
+++ b/novelwriter/types.py
@@ -65,6 +65,10 @@ QtPaintAnitAlias = QPainter.RenderHint.Antialiasing
QtMouseOver = QStyle.StateFlag.State_MouseOver
QtSelected = QStyle.StateFlag.State_Selected
+# Qt Colour Types
+
+QtHexRgb = QColor.NameFormat.HexRgb
+
# Qt Tree and Table Types
QtDecoration = Qt.ItemDataRole.DecorationRole
diff --git a/tests/reference/fmtToDocX_SaveDocument_styles.xml b/tests/reference/fmtToDocX_SaveDocument_styles.xml
index a8c8a16f..0fb4e06b 100644
--- a/tests/reference/fmtToDocX_SaveDocument_styles.xml
+++ b/tests/reference/fmtToDocX_SaveDocument_styles.xml
@@ -50,7 +50,7 @@
-
+
@@ -65,7 +65,7 @@
-
+
@@ -80,7 +80,7 @@
-
+
@@ -95,7 +95,7 @@
-
+
diff --git a/tests/reference/fmtToOdt_SaveFlat_document.fodt b/tests/reference/fmtToOdt_SaveFlat_document.fodt
index 14c9e9a4..6280be49 100644
--- a/tests/reference/fmtToOdt_SaveFlat_document.fodt
+++ b/tests/reference/fmtToOdt_SaveFlat_document.fodt
@@ -1,13 +1,13 @@
- 2024-10-17T22:22:08
+ 2024-10-22T11:39:53
novelWriter/2.6a1
Jane Smith
1234
P42DT12H34M56S
Test Project
- 2024-10-17T22:22:08
+ 2024-10-22T11:39:53
Jane Smith
@@ -47,19 +47,19 @@
-
+
-
+
-
+
-
+
diff --git a/tests/reference/fmtToOdt_SaveFull_styles.xml b/tests/reference/fmtToOdt_SaveFull_styles.xml
index 708be359..e66e2d7e 100644
--- a/tests/reference/fmtToOdt_SaveFull_styles.xml
+++ b/tests/reference/fmtToOdt_SaveFull_styles.xml
@@ -37,19 +37,19 @@
-
+
-
+
-
+
-
+
diff --git a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm
index 794071e8..9f9802ae 100644
--- a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm
+++ b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm
@@ -5,12 +5,12 @@
Lorem Ipsum
-
+
-
+
-
+
-
+
diff --git a/tests/test_formats/test_fmt_toodt.py b/tests/test_formats/test_fmt_toodt.py
index 7f75b935..971cddda 100644
--- a/tests/test_formats/test_fmt_toodt.py
+++ b/tests/test_formats/test_fmt_toodt.py
@@ -27,6 +27,8 @@ from shutil import copyfile
import pytest
+from PyQt5.QtGui import QColor
+
from novelwriter.common import xmlIndent
from novelwriter.constants import nwHeadFmt
from novelwriter.core.project import NWProject
@@ -1106,11 +1108,15 @@ def testFmtToOdt_ODTParagraphStyle():
assert parStyle._tAttr["color"] == ["fo", None]
assert parStyle._tAttr["opacity"] == ["loext", None]
- parStyle.setColour("#000000")
- parStyle.setOpacity("1.00")
+ parStyle.setColour(QColor(0, 0, 0, 128))
assert parStyle._tAttr["color"] == ["fo", "#000000"]
- assert parStyle._tAttr["opacity"] == ["loext", "1.00"]
+ assert parStyle._tAttr["opacity"] == ["loext", "50%"]
+
+ parStyle.setColour(None)
+
+ assert parStyle._tAttr["color"] == ["fo", None]
+ assert parStyle._tAttr["opacity"] == ["loext", None]
# Pack XML
# ========
@@ -1124,7 +1130,7 @@ def testFmtToOdt_ODTParagraphStyle():
'fo:margin-left="0.000cm" fo:margin-right="0.000cm" fo:text-indent="0.000cm" '
'fo:line-height="1.15" />'
''
+ 'fo:font-size="12pt" />'
''
''
)
@@ -1151,8 +1157,8 @@ def testFmtToOdt_ODTParagraphStyle():
aStyle = ODTParagraphStyle("test")
oStyle = ODTParagraphStyle("test")
- aStyle.setColour("#000000")
- oStyle.setColour("#111111")
+ aStyle.setColour(QColor(0, 0, 0))
+ oStyle.setColour(QColor(42, 42, 42))
assert aStyle.checkNew(oStyle) is True
assert aStyle.getID() != oStyle.getID()
diff --git a/tests/test_formats/test_fmt_toqdoc.py b/tests/test_formats/test_fmt_toqdoc.py
index 67417da8..48c3b6cb 100644
--- a/tests/test_formats/test_fmt_toqdoc.py
+++ b/tests/test_formats/test_fmt_toqdoc.py
@@ -27,8 +27,8 @@ from PyQt5.QtGui import QTextBlock, QTextCharFormat, QTextCursor
from novelwriter import CONFIG
from novelwriter.constants import nwUnicode
from novelwriter.core.project import NWProject
-from novelwriter.formats.shared import BlockFmt, BlockTyp
-from novelwriter.formats.toqdoc import TextDocumentTheme, ToQTextDocument
+from novelwriter.formats.shared import BlockFmt, BlockTyp, TextDocumentTheme
+from novelwriter.formats.toqdoc import ToQTextDocument
from novelwriter.types import (
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
QtPageBreakAfter, QtTransparent, QtVAlignNormal, QtVAlignSub, QtVAlignSuper