novelWriter 2.5b1__py3-none-any.whl → 2.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/RECORD +77 -75
  3. {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +3 -3
  5. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  6. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  7. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  8. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  9. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  10. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  11. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  12. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  13. novelwriter/assets/i18n/nw_pl_PL.qm +0 -0
  14. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  15. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  16. novelwriter/assets/i18n/project_pl_PL.json +116 -0
  17. novelwriter/assets/i18n/project_pt_BR.json +74 -74
  18. novelwriter/assets/manual.pdf +0 -0
  19. novelwriter/assets/sample.zip +0 -0
  20. novelwriter/assets/text/credits_en.htm +52 -44
  21. novelwriter/assets/themes/cyberpunk_night.conf +1 -0
  22. novelwriter/assets/themes/default_dark.conf +1 -0
  23. novelwriter/assets/themes/default_light.conf +1 -0
  24. novelwriter/assets/themes/dracula.conf +1 -0
  25. novelwriter/assets/themes/solarized_dark.conf +1 -0
  26. novelwriter/assets/themes/solarized_light.conf +1 -0
  27. novelwriter/common.py +12 -3
  28. novelwriter/config.py +67 -15
  29. novelwriter/constants.py +8 -10
  30. novelwriter/core/buildsettings.py +5 -3
  31. novelwriter/core/coretools.py +3 -1
  32. novelwriter/core/docbuild.py +1 -0
  33. novelwriter/core/project.py +15 -4
  34. novelwriter/core/status.py +4 -1
  35. novelwriter/core/storage.py +6 -1
  36. novelwriter/core/tohtml.py +69 -29
  37. novelwriter/core/tokenizer.py +83 -14
  38. novelwriter/core/toodt.py +48 -21
  39. novelwriter/core/toqdoc.py +37 -21
  40. novelwriter/dialogs/about.py +10 -15
  41. novelwriter/dialogs/docmerge.py +16 -16
  42. novelwriter/dialogs/docsplit.py +16 -16
  43. novelwriter/dialogs/editlabel.py +6 -8
  44. novelwriter/dialogs/preferences.py +106 -93
  45. novelwriter/dialogs/projectsettings.py +16 -20
  46. novelwriter/dialogs/quotes.py +9 -5
  47. novelwriter/dialogs/wordlist.py +6 -6
  48. novelwriter/enum.py +4 -5
  49. novelwriter/extensions/configlayout.py +38 -4
  50. novelwriter/extensions/modified.py +22 -3
  51. novelwriter/extensions/{circularprogress.py → progressbars.py} +26 -3
  52. novelwriter/extensions/statusled.py +39 -23
  53. novelwriter/gui/doceditor.py +22 -13
  54. novelwriter/gui/dochighlight.py +30 -39
  55. novelwriter/gui/docviewer.py +24 -15
  56. novelwriter/gui/docviewerpanel.py +7 -0
  57. novelwriter/gui/mainmenu.py +11 -11
  58. novelwriter/gui/outline.py +4 -3
  59. novelwriter/gui/projtree.py +85 -77
  60. novelwriter/gui/search.py +10 -1
  61. novelwriter/gui/statusbar.py +25 -29
  62. novelwriter/gui/theme.py +3 -0
  63. novelwriter/guimain.py +139 -124
  64. novelwriter/shared.py +19 -8
  65. novelwriter/text/patterns.py +113 -0
  66. novelwriter/tools/dictionaries.py +2 -8
  67. novelwriter/tools/lipsum.py +8 -12
  68. novelwriter/tools/manusbuild.py +9 -9
  69. novelwriter/tools/manuscript.py +10 -5
  70. novelwriter/tools/manussettings.py +7 -3
  71. novelwriter/tools/noveldetails.py +10 -10
  72. novelwriter/tools/welcome.py +19 -10
  73. novelwriter/tools/writingstats.py +3 -3
  74. novelwriter/types.py +5 -2
  75. novelwriter/extensions/simpleprogress.py +0 -53
  76. {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/LICENSE.md +0 -0
  77. {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/entry_points.txt +0 -0
  78. {novelWriter-2.5b1.dist-info → novelWriter-2.5.1.dist-info}/top_level.txt +0 -0
@@ -29,7 +29,6 @@ import logging
29
29
  from pathlib import Path
30
30
  from time import time
31
31
 
32
- from novelwriter import CONFIG
33
32
  from novelwriter.common import formatTimeStamp
34
33
  from novelwriter.constants import nwHeadFmt, nwHtmlUnicode, nwKeyWords, nwLabels
35
34
  from novelwriter.core.project import NWProject
@@ -38,24 +37,35 @@ from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
38
37
 
39
38
  logger = logging.getLogger(__name__)
40
39
 
41
- HTML5_TAGS = {
42
- Tokenizer.FMT_B_B: "<strong>",
43
- Tokenizer.FMT_B_E: "</strong>",
44
- Tokenizer.FMT_I_B: "<em>",
45
- Tokenizer.FMT_I_E: "</em>",
46
- Tokenizer.FMT_D_B: "<del>",
47
- Tokenizer.FMT_D_E: "</del>",
48
- Tokenizer.FMT_U_B: "<span style='text-decoration: underline;'>",
49
- Tokenizer.FMT_U_E: "</span>",
50
- Tokenizer.FMT_M_B: "<mark>",
51
- Tokenizer.FMT_M_E: "</mark>",
52
- Tokenizer.FMT_SUP_B: "<sup>",
53
- Tokenizer.FMT_SUP_E: "</sup>",
54
- Tokenizer.FMT_SUB_B: "<sub>",
55
- Tokenizer.FMT_SUB_E: "</sub>",
56
- Tokenizer.FMT_STRIP: "",
40
+ # Each opener tag, with the id of its corresponding closer and tag format
41
+ HTML_OPENER: dict[int, tuple[int, str]] = {
42
+ Tokenizer.FMT_B_B: (Tokenizer.FMT_B_E, "<strong>"),
43
+ Tokenizer.FMT_I_B: (Tokenizer.FMT_I_E, "<em>"),
44
+ Tokenizer.FMT_D_B: (Tokenizer.FMT_D_E, "<del>"),
45
+ Tokenizer.FMT_U_B: (Tokenizer.FMT_U_E, "<span style='text-decoration: underline;'>"),
46
+ Tokenizer.FMT_M_B: (Tokenizer.FMT_M_E, "<mark>"),
47
+ Tokenizer.FMT_SUP_B: (Tokenizer.FMT_SUP_E, "<sup>"),
48
+ Tokenizer.FMT_SUB_B: (Tokenizer.FMT_SUB_E, "<sub>"),
49
+ Tokenizer.FMT_DL_B: (Tokenizer.FMT_DL_E, "<span class='dialog'>"),
50
+ Tokenizer.FMT_ADL_B: (Tokenizer.FMT_ADL_E, "<span class='altdialog'>"),
57
51
  }
58
52
 
53
+ # Each closer tag, with the id of its corresponding opener and tag format
54
+ HTML_CLOSER: dict[int, tuple[int, str]] = {
55
+ Tokenizer.FMT_B_E: (Tokenizer.FMT_B_B, "</strong>"),
56
+ Tokenizer.FMT_I_E: (Tokenizer.FMT_I_B, "</em>"),
57
+ Tokenizer.FMT_D_E: (Tokenizer.FMT_D_B, "</del>"),
58
+ Tokenizer.FMT_U_E: (Tokenizer.FMT_U_B, "</span>"),
59
+ Tokenizer.FMT_M_E: (Tokenizer.FMT_M_B, "</mark>"),
60
+ Tokenizer.FMT_SUP_E: (Tokenizer.FMT_SUP_B, "</sup>"),
61
+ Tokenizer.FMT_SUB_E: (Tokenizer.FMT_SUB_B, "</sub>"),
62
+ Tokenizer.FMT_DL_E: (Tokenizer.FMT_DL_B, "</span>"),
63
+ Tokenizer.FMT_ADL_E: (Tokenizer.FMT_ADL_B, "</span>"),
64
+ }
65
+
66
+ # Empty HTML tag record
67
+ HTML_NONE = (0, "")
68
+
59
69
 
60
70
  class ToHtml(Tokenizer):
61
71
  """Core: HTML Document Writer
@@ -184,7 +194,6 @@ class ToHtml(Tokenizer):
184
194
 
185
195
  if tStyle & self.A_PBB:
186
196
  aStyle.append("page-break-before: always;")
187
-
188
197
  if tStyle & self.A_PBA:
189
198
  aStyle.append("page-break-after: always;")
190
199
 
@@ -194,11 +203,13 @@ class ToHtml(Tokenizer):
194
203
  aStyle.append("margin-top: 0;")
195
204
 
196
205
  if tStyle & self.A_IND_L:
197
- aStyle.append(f"margin-left: {CONFIG.tabWidth:d}px;")
206
+ aStyle.append(f"margin-left: {self._blockIndent:.2f}em;")
198
207
  if tStyle & self.A_IND_R:
199
- aStyle.append(f"margin-right: {CONFIG.tabWidth:d}px;")
208
+ aStyle.append(f"margin-right: {self._blockIndent:.2f}em;")
209
+ if tStyle & self.A_IND_T:
210
+ aStyle.append(f"text-indent: {self._firstWidth:.2f}em;")
200
211
 
201
- if len(aStyle) > 0:
212
+ if aStyle:
202
213
  stVals = " ".join(aStyle)
203
214
  hStyle = f" style='{stVals}'"
204
215
  else:
@@ -431,6 +442,8 @@ class ToHtml(Tokenizer):
431
442
  styles.append(".break {text-align: left;}")
432
443
  styles.append(".synopsis {font-style: italic;}")
433
444
  styles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}")
445
+ styles.append(".dialog {color: rgb(66, 113, 174);}")
446
+ styles.append(".altdialog {color: rgb(129, 55, 9);}")
434
447
 
435
448
  return styles
436
449
 
@@ -441,19 +454,46 @@ class ToHtml(Tokenizer):
441
454
  def _formatText(self, text: str, tFmt: T_Formats) -> str:
442
455
  """Apply formatting tags to text."""
443
456
  temp = text
444
- for pos, fmt, data in reversed(tFmt):
445
- html = ""
446
- if fmt == self.FMT_FNOTE:
457
+
458
+ # Build a list of all html tags that need to be inserted in the text.
459
+ # This is done in the forward direction, and a tag is only opened if it
460
+ # isn't already open, and only closed if it has previously been opened.
461
+ tags: list[tuple[int, str]] = []
462
+ state = dict.fromkeys(HTML_OPENER, False)
463
+ for pos, fmt, data in tFmt:
464
+ if m := HTML_OPENER.get(fmt):
465
+ if not state.get(fmt, True):
466
+ tags.append((pos, m[1]))
467
+ state[fmt] = True
468
+ elif m := HTML_CLOSER.get(fmt):
469
+ if state.get(m[0], False):
470
+ tags.append((pos, m[1]))
471
+ state[m[0]] = False
472
+ elif fmt == self.FMT_FNOTE:
447
473
  if data in self._footnotes:
448
474
  index = len(self._usedNotes) + 1
449
475
  self._usedNotes[data] = index
450
- html = f"<sup><a href='#footnote_{index}'>{index}</a></sup>"
476
+ tags.append((pos, f"<sup><a href='#footnote_{index}'>{index}</a></sup>"))
451
477
  else:
452
- html = "<sup>ERR</sup>"
453
- else:
454
- html = HTML5_TAGS.get(fmt, "ERR")
455
- temp = f"{temp[:pos]}{html}{temp[pos:]}"
478
+ tags.append((pos, "<sup>ERR</sup>"))
479
+
480
+ # Check all format types and close any tag that is still open. This
481
+ # ensures that unclosed tags don't spill over to the next paragraph.
482
+ end = len(text)
483
+ for opener, active in state.items():
484
+ if active:
485
+ closer = HTML_OPENER.get(opener, HTML_NONE)[0]
486
+ tags.append((end, HTML_CLOSER.get(closer, HTML_NONE)[1]))
487
+
488
+ # Insert all tags at their correct position, starting from the back.
489
+ # The reverse order ensures that the positions are not shifted while we
490
+ # insert tags.
491
+ for pos, tag in reversed(tags):
492
+ temp = f"{temp[:pos]}{tag}{temp[pos:]}"
493
+
494
+ # Replace all line breaks with proper HTML break tags
456
495
  temp = temp.replace("\n", "<br>")
496
+
457
497
  return stripEscape(temp)
458
498
 
459
499
  def _formatSynopsis(self, text: str, synopsis: bool) -> str:
@@ -36,13 +36,13 @@ from time import time
36
36
  from PyQt5.QtCore import QCoreApplication, QRegularExpression
37
37
  from PyQt5.QtGui import QFont
38
38
 
39
+ from novelwriter import CONFIG
39
40
  from novelwriter.common import checkInt, formatTimeStamp, numberToRoman
40
- from novelwriter.constants import (
41
- nwHeadFmt, nwKeyWords, nwLabels, nwRegEx, nwShortcode, nwUnicode, trConst
42
- )
41
+ from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwShortcode, nwUnicode, trConst
43
42
  from novelwriter.core.index import processComment
44
43
  from novelwriter.core.project import NWProject
45
44
  from novelwriter.enum import nwComment, nwItemLayout
45
+ from novelwriter.text.patterns import REGEX_PATTERNS
46
46
 
47
47
  logger = logging.getLogger(__name__)
48
48
 
@@ -85,8 +85,12 @@ class Tokenizer(ABC):
85
85
  FMT_SUP_E = 12 # End superscript
86
86
  FMT_SUB_B = 13 # Begin subscript
87
87
  FMT_SUB_E = 14 # End subscript
88
- FMT_FNOTE = 15 # Footnote marker
89
- FMT_STRIP = 16 # Strip the format code
88
+ FMT_DL_B = 15 # Begin dialogue
89
+ FMT_DL_E = 16 # End dialogue
90
+ FMT_ADL_B = 17 # Begin alt dialogue
91
+ FMT_ADL_E = 18 # End alt dialogue
92
+ FMT_FNOTE = 19 # Footnote marker
93
+ FMT_STRIP = 20 # Strip the format code
90
94
 
91
95
  # Block Type
92
96
  T_EMPTY = 1 # Empty line (new paragraph)
@@ -115,6 +119,10 @@ class Tokenizer(ABC):
115
119
  A_Z_BTMMRG = 0x0080 # Zero bottom margin
116
120
  A_IND_L = 0x0100 # Left indentation
117
121
  A_IND_R = 0x0200 # Right indentation
122
+ A_IND_T = 0x0400 # Text indentation
123
+
124
+ # Masks
125
+ M_ALIGNED = A_LEFT | A_RIGHT | A_CENTRE | A_JUSTIFY
118
126
 
119
127
  # Lookups
120
128
  L_HEADINGS = [T_TITLE, T_HEAD1, T_HEAD2, T_HEAD3, T_HEAD4]
@@ -189,7 +197,9 @@ class Tokenizer(ABC):
189
197
 
190
198
  # Instance Variables
191
199
  self._hFormatter = HeadingFormatter(self._project)
192
- self._noSep = True # Flag to indicate that we don't want a scene separator
200
+ self._noSep = True # Flag to indicate that we don't want a scene separator
201
+ self._noIndent = False # Flag to disable text indent on next paragraph
202
+ self._showDialog = False # Flag for dialogue highlighting
193
203
 
194
204
  # This File
195
205
  self._isNovel = False # Document is a novel document
@@ -204,12 +214,12 @@ class Tokenizer(ABC):
204
214
 
205
215
  # Format RegEx
206
216
  self._rxMarkdown = [
207
- (QRegularExpression(nwRegEx.FMT_EI), [0, self.FMT_I_B, 0, self.FMT_I_E]),
208
- (QRegularExpression(nwRegEx.FMT_EB), [0, self.FMT_B_B, 0, self.FMT_B_E]),
209
- (QRegularExpression(nwRegEx.FMT_ST), [0, self.FMT_D_B, 0, self.FMT_D_E]),
217
+ (REGEX_PATTERNS.markdownItalic, [0, self.FMT_I_B, 0, self.FMT_I_E]),
218
+ (REGEX_PATTERNS.markdownBold, [0, self.FMT_B_B, 0, self.FMT_B_E]),
219
+ (REGEX_PATTERNS.markdownStrike, [0, self.FMT_D_B, 0, self.FMT_D_E]),
210
220
  ]
211
- self._rxShortCodes = QRegularExpression(nwRegEx.FMT_SC)
212
- self._rxShortCodeVals = QRegularExpression(nwRegEx.FMT_SV)
221
+ self._rxShortCodes = REGEX_PATTERNS.shortcodePlain
222
+ self._rxShortCodeVals = REGEX_PATTERNS.shortcodeValue
213
223
 
214
224
  self._shortCodeFmt = {
215
225
  nwShortcode.ITALIC_O: self.FMT_I_B, nwShortcode.ITALIC_C: self.FMT_I_E,
@@ -224,6 +234,8 @@ class Tokenizer(ABC):
224
234
  nwShortcode.FOOTNOTE_B: self.FMT_FNOTE,
225
235
  }
226
236
 
237
+ self._rxDialogue: list[tuple[QRegularExpression, int, int]] = []
238
+
227
239
  return
228
240
 
229
241
  ##
@@ -345,6 +357,29 @@ class Tokenizer(ABC):
345
357
  self._doJustify = state
346
358
  return
347
359
 
360
+ def setDialogueHighlight(self, state: bool) -> None:
361
+ """Enable or disable dialogue highlighting."""
362
+ self._rxDialogue = []
363
+ self._showDialog = state
364
+ if state:
365
+ if CONFIG.dialogStyle > 0:
366
+ self._rxDialogue.append((
367
+ REGEX_PATTERNS.dialogStyle, self.FMT_DL_B, self.FMT_DL_E
368
+ ))
369
+ if CONFIG.dialogLine:
370
+ self._rxDialogue.append((
371
+ REGEX_PATTERNS.dialogLine, self.FMT_DL_B, self.FMT_DL_E
372
+ ))
373
+ if CONFIG.narratorBreak:
374
+ self._rxDialogue.append((
375
+ REGEX_PATTERNS.narratorBreak, self.FMT_DL_E, self.FMT_DL_B
376
+ ))
377
+ if CONFIG.altDialogOpen and CONFIG.altDialogClose:
378
+ self._rxDialogue.append((
379
+ REGEX_PATTERNS.altDialogStyle, self.FMT_ADL_B, self.FMT_ADL_E
380
+ ))
381
+ return
382
+
348
383
  def setTitleMargins(self, upper: float, lower: float) -> None:
349
384
  """Set the upper and lower title margin."""
350
385
  self._marginTitle = (float(upper), float(lower))
@@ -481,7 +516,7 @@ class Tokenizer(ABC):
481
516
  self._text = xRep.sub(lambda x: repDict[x.group(0)], self._text)
482
517
 
483
518
  # Process the character translation map
484
- trDict = {nwUnicode.U_MAPOSS: nwUnicode.U_RSQUO}
519
+ trDict = {nwUnicode.U_MAPOS: nwUnicode.U_RSQUO}
485
520
  self._text = self._text.translate(str.maketrans(trDict))
486
521
 
487
522
  return
@@ -846,6 +881,12 @@ class Tokenizer(ABC):
846
881
  if n < tCount - 1:
847
882
  nToken = tokens[n+1] # Look ahead
848
883
 
884
+ if cToken[0] in self.L_SKIP_INDENT and not self._indentFirst:
885
+ # Unless the indentFirst flag is set, we set up the next
886
+ # paragraph to not be indented if we see a block of a
887
+ # specific type
888
+ self._noIndent = True
889
+
849
890
  if cToken[0] == self.T_EMPTY:
850
891
  # We don't need to keep the empty lines after this pass
851
892
  pass
@@ -864,11 +905,27 @@ class Tokenizer(ABC):
864
905
  elif cToken[0] == self.T_TEXT:
865
906
  # Combine lines from the same paragraph
866
907
  pLines.append(cToken)
908
+
867
909
  if nToken[0] != self.T_TEXT:
910
+ # Next token is not text, so we add the buffer to tokens
868
911
  nLines = len(pLines)
912
+ cStyle = pLines[0][4]
913
+ if self._firstIndent and not (self._noIndent or cStyle & self.M_ALIGNED):
914
+ # If paragraph indentation is enabled, not temporarily
915
+ # turned off, and the block is not aligned, we add the
916
+ # text indentation flag
917
+ cStyle |= self.A_IND_T
918
+
869
919
  if nLines == 1:
870
- self._tokens.append(pLines[0])
920
+ # The paragraph contains a single line, so we just
921
+ # save that directly to the token list
922
+ self._tokens.append((
923
+ self.T_TEXT, pLines[0][1], pLines[0][2], pLines[0][3], cStyle
924
+ ))
871
925
  elif nLines > 1:
926
+ # The paragraph contains multiple lines, so we need to
927
+ # join them according to the line break policy, and
928
+ # recompute all the formatting markers
872
929
  tTxt = ""
873
930
  tFmt: T_Formats = []
874
931
  for aToken in pLines:
@@ -876,9 +933,12 @@ class Tokenizer(ABC):
876
933
  tTxt += f"{aToken[2]}{lineSep}"
877
934
  tFmt.extend((p+tLen, fmt, key) for p, fmt, key in aToken[3])
878
935
  self._tokens.append((
879
- self.T_TEXT, pLines[0][1], tTxt[:-1], tFmt, pLines[0][4]
936
+ self.T_TEXT, pLines[0][1], tTxt[:-1], tFmt, cStyle
880
937
  ))
938
+
939
+ # Reset buffer and make sure text indent is on for next pass
881
940
  pLines = []
941
+ self._noIndent = False
882
942
 
883
943
  else:
884
944
  self._tokens.append(cToken)
@@ -1076,6 +1136,15 @@ class Tokenizer(ABC):
1076
1136
  f"{tHandle}:{rxMatch.captured(2)}",
1077
1137
  ))
1078
1138
 
1139
+ # Match Dialogue
1140
+ if self._rxDialogue:
1141
+ for regEx, fmtB, fmtE in self._rxDialogue:
1142
+ rxItt = regEx.globalMatch(text, 0)
1143
+ while rxItt.hasNext():
1144
+ rxMatch = rxItt.next()
1145
+ temp.append((rxMatch.capturedStart(0), 0, fmtB, ""))
1146
+ temp.append((rxMatch.capturedEnd(0), 0, fmtE, ""))
1147
+
1079
1148
  # Post-process text and format
1080
1149
  result = text
1081
1150
  formats = []
novelwriter/core/toodt.py CHANGED
@@ -82,13 +82,15 @@ TAG_SPAN = _mkTag("text", "span")
82
82
  TAG_STNM = _mkTag("text", "style-name")
83
83
 
84
84
  # Formatting Codes
85
- X_BLD = 0x01 # Bold format
86
- X_ITA = 0x02 # Italic format
87
- X_DEL = 0x04 # Strikethrough format
88
- X_UND = 0x08 # Underline format
89
- X_MRK = 0x10 # Marked format
90
- X_SUP = 0x20 # Superscript
91
- X_SUB = 0x40 # Subscript
85
+ X_BLD = 0x001 # Bold format
86
+ X_ITA = 0x002 # Italic format
87
+ X_DEL = 0x004 # Strikethrough format
88
+ X_UND = 0x008 # Underline format
89
+ X_MRK = 0x010 # Marked format
90
+ X_SUP = 0x020 # Superscript
91
+ X_SUB = 0x040 # Subscript
92
+ X_DLG = 0x080 # Dialogue
93
+ X_DLA = 0x100 # Alt. Dialogue
92
94
 
93
95
  # Formatting Masks
94
96
  M_BLD = ~X_BLD
@@ -98,6 +100,8 @@ M_UND = ~X_UND
98
100
  M_MRK = ~X_MRK
99
101
  M_SUP = ~X_SUP
100
102
  M_SUB = ~X_SUB
103
+ M_DLG = ~X_DLG
104
+ M_DLA = ~X_DLA
101
105
 
102
106
  # ODT Styles
103
107
  S_TITLE = "Title"
@@ -216,13 +220,15 @@ class ToOdt(Tokenizer):
216
220
  self._mDocRight = "2.000cm"
217
221
 
218
222
  # Colour
219
- self._colHead12 = None
220
- self._opaHead12 = None
221
- self._colHead34 = None
222
- self._opaHead34 = None
223
- self._colMetaTx = None
224
- self._opaMetaTx = None
225
- self._markText = "#ffffa6"
223
+ self._colHead12 = None
224
+ self._opaHead12 = None
225
+ self._colHead34 = None
226
+ self._opaHead34 = None
227
+ self._colMetaTx = None
228
+ self._opaMetaTx = None
229
+ self._colDialogM = None
230
+ self._colDialogA = None
231
+ self._markText = "#ffffa6"
226
232
 
227
233
  return
228
234
 
@@ -324,6 +330,10 @@ class ToOdt(Tokenizer):
324
330
  self._colMetaTx = "#813709"
325
331
  self._opaMetaTx = "100%"
326
332
 
333
+ if self._showDialog:
334
+ self._colDialogM = "#2a6099"
335
+ self._colDialogA = "#813709"
336
+
327
337
  self._fLineHeight = f"{round(100 * self._lineHeight):d}%"
328
338
  self._fBlockIndent = self._emToCm(self._blockIndent)
329
339
  self._fTextIndent = self._emToCm(self._firstWidth)
@@ -438,7 +448,6 @@ class ToOdt(Tokenizer):
438
448
  self._result = "" # Not used, but cleared just in case
439
449
 
440
450
  xText = self._xText
441
- pIndent = True
442
451
  for tType, _, tText, tFormat, tStyle in self._tokens:
443
452
 
444
453
  # Styles
@@ -455,7 +464,6 @@ class ToOdt(Tokenizer):
455
464
 
456
465
  if tStyle & self.A_PBB:
457
466
  oStyle.setBreakBefore("page")
458
-
459
467
  if tStyle & self.A_PBA:
460
468
  oStyle.setBreakAfter("page")
461
469
 
@@ -469,16 +477,14 @@ class ToOdt(Tokenizer):
469
477
  if tStyle & self.A_IND_R:
470
478
  oStyle.setMarginRight(self._fBlockIndent)
471
479
 
472
- if not self._indentFirst and tType in self.L_SKIP_INDENT:
473
- pIndent = False
474
-
475
480
  # Process Text Types
476
481
  if tType == self.T_TEXT:
477
- if self._firstIndent and pIndent and oStyle.isUnaligned():
482
+ # Text indentation is processed here because there is a
483
+ # dedicated pre-defined style for it
484
+ if tStyle & self.A_IND_T:
478
485
  self._addTextPar(xText, S_FIND, oStyle, tText, tFmt=tFormat)
479
486
  else:
480
487
  self._addTextPar(xText, S_TEXT, oStyle, tText, tFmt=tFormat)
481
- pIndent = True
482
488
 
483
489
  elif tType == self.T_TITLE:
484
490
  # Title must be text:p
@@ -688,6 +694,14 @@ class ToOdt(Tokenizer):
688
694
  xFmt |= X_SUB
689
695
  elif fFmt == self.FMT_SUB_E:
690
696
  xFmt &= M_SUB
697
+ elif fFmt == self.FMT_DL_B:
698
+ xFmt |= X_DLG
699
+ elif fFmt == self.FMT_DL_E:
700
+ xFmt &= M_DLG
701
+ elif fFmt == self.FMT_ADL_B:
702
+ xFmt |= X_DLA
703
+ elif fFmt == self.FMT_ADL_E:
704
+ xFmt &= M_DLA
691
705
  elif fFmt == self.FMT_FNOTE:
692
706
  xNode = self._generateFootnote(fData)
693
707
  elif fFmt == self.FMT_STRIP:
@@ -761,6 +775,10 @@ class ToOdt(Tokenizer):
761
775
  style.setTextPosition("super")
762
776
  if hFmt & X_SUB:
763
777
  style.setTextPosition("sub")
778
+ if hFmt & X_DLG:
779
+ style.setColour(self._colDialogM)
780
+ if hFmt & X_DLA:
781
+ style.setColour(self._colDialogA)
764
782
  self._autoText[hFmt] = style
765
783
 
766
784
  return style.name
@@ -1361,6 +1379,7 @@ class ODTTextStyle:
1361
1379
  self._tAttr = {
1362
1380
  "font-weight": ["fo", None],
1363
1381
  "font-style": ["fo", None],
1382
+ "color": ["fo", None],
1364
1383
  "background-color": ["fo", None],
1365
1384
  "text-position": ["style", None],
1366
1385
  "text-line-through-style": ["style", None],
@@ -1395,6 +1414,14 @@ class ODTTextStyle:
1395
1414
  self._tAttr["font-style"][1] = None
1396
1415
  return
1397
1416
 
1417
+ def setColour(self, value: str | None) -> None:
1418
+ """Set text colour."""
1419
+ if value and len(value) == 7 and value[0] == "#":
1420
+ self._tAttr["color"][1] = value
1421
+ else:
1422
+ self._tAttr["color"][1] = None
1423
+ return
1424
+
1398
1425
  def setBackgroundColour(self, value: str | None) -> None:
1399
1426
  """Set text background colour."""
1400
1427
  if value and len(value) == 7 and value[0] == "#":
@@ -26,7 +26,7 @@ from __future__ import annotations
26
26
  import logging
27
27
 
28
28
  from PyQt5.QtGui import (
29
- QColor, QFont, QFontMetrics, QTextBlockFormat, QTextCharFormat,
29
+ QColor, QFont, QFontMetricsF, QTextBlockFormat, QTextCharFormat,
30
30
  QTextCursor, QTextDocument
31
31
  )
32
32
 
@@ -45,16 +45,18 @@ T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
45
45
 
46
46
 
47
47
  class TextDocumentTheme:
48
- text: QColor = QtBlack
48
+ text: QColor = QtBlack
49
49
  highlight: QColor = QtTransparent
50
- head: QColor = QtBlack
51
- comment: QColor = QtBlack
52
- note: QColor = QtBlack
53
- code: QColor = QtBlack
54
- modifier: QColor = QtBlack
55
- keyword: QColor = QtBlack
56
- tag: QColor = QtBlack
57
- optional: QColor = QtBlack
50
+ head: QColor = QtBlack
51
+ comment: QColor = QtBlack
52
+ note: QColor = QtBlack
53
+ code: QColor = QtBlack
54
+ modifier: QColor = QtBlack
55
+ keyword: QColor = QtBlack
56
+ tag: QColor = QtBlack
57
+ optional: QColor = QtBlack
58
+ dialog: QColor = QtBlack
59
+ altdialog: QColor = QtBlack
58
60
 
59
61
 
60
62
  def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
@@ -97,19 +99,19 @@ class ToQTextDocument(Tokenizer):
97
99
  self._document.clear()
98
100
  self._document.setDefaultFont(self._textFont)
99
101
 
100
- qMetric = QFontMetrics(self._textFont)
101
- mScale = qMetric.height()
102
+ qMetric = QFontMetricsF(self._textFont)
103
+ mPx = qMetric.ascent() # 1 em in pixels
102
104
  fPt = self._textFont.pointSizeF()
103
105
 
104
106
  # Scaled Sizes
105
107
  # ============
106
108
 
107
109
  self._mHead = {
108
- self.T_TITLE: (mScale * self._marginTitle[0], mScale * self._marginTitle[1]),
109
- self.T_HEAD1: (mScale * self._marginHead1[0], mScale * self._marginHead1[1]),
110
- self.T_HEAD2: (mScale * self._marginHead2[0], mScale * self._marginHead2[1]),
111
- self.T_HEAD3: (mScale * self._marginHead3[0], mScale * self._marginHead3[1]),
112
- self.T_HEAD4: (mScale * self._marginHead4[0], mScale * self._marginHead4[1]),
110
+ self.T_TITLE: (mPx * self._marginTitle[0], mPx * self._marginTitle[1]),
111
+ self.T_HEAD1: (mPx * self._marginHead1[0], mPx * self._marginHead1[1]),
112
+ self.T_HEAD2: (mPx * self._marginHead2[0], mPx * self._marginHead2[1]),
113
+ self.T_HEAD3: (mPx * self._marginHead3[0], mPx * self._marginHead3[1]),
114
+ self.T_HEAD4: (mPx * self._marginHead4[0], mPx * self._marginHead4[1]),
113
115
  }
114
116
 
115
117
  self._sHead = {
@@ -120,11 +122,12 @@ class ToQTextDocument(Tokenizer):
120
122
  self.T_HEAD4: nwHeaders.H_SIZES.get(4, 1.0) * fPt,
121
123
  }
122
124
 
123
- self._mText = (mScale * self._marginText[0], mScale * self._marginText[1])
124
- self._mMeta = (mScale * self._marginMeta[0], mScale * self._marginMeta[1])
125
- self._mSep = (mScale * self._marginSep[0], mScale * self._marginSep[1])
125
+ self._mText = (mPx * self._marginText[0], mPx * self._marginText[1])
126
+ self._mMeta = (mPx * self._marginMeta[0], mPx * self._marginMeta[1])
127
+ self._mSep = (mPx * self._marginSep[0], mPx * self._marginSep[1])
126
128
 
127
- self._mIndent = mScale * 2.0
129
+ self._mIndent = mPx * 2.0
130
+ self._tIndent = mPx * self._firstWidth
128
131
 
129
132
  # Block Format
130
133
  # ============
@@ -133,6 +136,9 @@ class ToQTextDocument(Tokenizer):
133
136
  self._blockFmt.setTopMargin(self._mText[0])
134
137
  self._blockFmt.setBottomMargin(self._mText[1])
135
138
  self._blockFmt.setAlignment(QtAlignJustify if self._doJustify else QtAlignAbsolute)
139
+ self._blockFmt.setLineHeight(
140
+ 100*self._lineHeight, QTextBlockFormat.LineHeightTypes.ProportionalHeight
141
+ )
136
142
 
137
143
  # Character Formats
138
144
  # =================
@@ -224,6 +230,8 @@ class ToQTextDocument(Tokenizer):
224
230
  bFmt.setLeftMargin(self._mIndent)
225
231
  if tStyle & self.A_IND_R:
226
232
  bFmt.setRightMargin(self._mIndent)
233
+ if tStyle & self.A_IND_T:
234
+ bFmt.setTextIndent(self._tIndent)
227
235
 
228
236
  if tType == self.T_TEXT:
229
237
  newBlock(cursor, bFmt)
@@ -337,6 +345,14 @@ class ToQTextDocument(Tokenizer):
337
345
  cFmt.setVerticalAlignment(QtVAlignSub)
338
346
  elif fmt == self.FMT_SUB_E:
339
347
  cFmt.setVerticalAlignment(QtVAlignNormal)
348
+ elif fmt == self.FMT_DL_B:
349
+ cFmt.setForeground(self._theme.dialog)
350
+ elif fmt == self.FMT_DL_E:
351
+ cFmt.setForeground(self._theme.text)
352
+ elif fmt == self.FMT_ADL_B:
353
+ cFmt.setForeground(self._theme.altdialog)
354
+ elif fmt == self.FMT_ADL_E:
355
+ cFmt.setForeground(self._theme.text)
340
356
  elif fmt == self.FMT_FNOTE:
341
357
  xFmt = QTextCharFormat(self._cCode)
342
358
  xFmt.setVerticalAlignment(QtVAlignSuper)