novelWriter 2.2.1__py3-none-any.whl → 2.3b1__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 (110) hide show
  1. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/RECORD +102 -92
  3. novelwriter/__init__.py +4 -4
  4. novelwriter/assets/icons/typicons_dark/icons.conf +6 -0
  5. novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
  6. novelwriter/assets/icons/typicons_dark/typ_document-add-col.svg +8 -0
  7. novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
  8. novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
  9. novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
  10. novelwriter/assets/icons/typicons_light/icons.conf +6 -0
  11. novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
  12. novelwriter/assets/icons/typicons_light/typ_document-add-col.svg +8 -0
  13. novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
  14. novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
  15. novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
  16. novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
  17. novelwriter/assets/images/novelwriter-text-light.svg +4 -0
  18. novelwriter/assets/images/welcome-dark.jpg +0 -0
  19. novelwriter/assets/images/welcome-light.jpg +0 -0
  20. novelwriter/assets/manual.pdf +0 -0
  21. novelwriter/assets/sample.zip +0 -0
  22. novelwriter/assets/syntax/default_dark.conf +1 -0
  23. novelwriter/assets/syntax/default_light.conf +1 -0
  24. novelwriter/assets/syntax/grey_dark.conf +1 -0
  25. novelwriter/assets/syntax/grey_light.conf +1 -0
  26. novelwriter/assets/syntax/light_owl.conf +1 -0
  27. novelwriter/assets/syntax/night_owl.conf +1 -0
  28. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  29. novelwriter/assets/syntax/solarized_light.conf +1 -0
  30. novelwriter/assets/syntax/tomorrow.conf +1 -0
  31. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  32. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  33. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  34. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  35. novelwriter/assets/text/credits_en.htm +4 -2
  36. novelwriter/assets/themes/default_dark.conf +2 -2
  37. novelwriter/assets/themes/default_light.conf +2 -2
  38. novelwriter/common.py +48 -37
  39. novelwriter/config.py +36 -41
  40. novelwriter/constants.py +38 -16
  41. novelwriter/core/buildsettings.py +7 -7
  42. novelwriter/core/coretools.py +192 -154
  43. novelwriter/core/docbuild.py +6 -3
  44. novelwriter/core/document.py +6 -6
  45. novelwriter/core/index.py +89 -56
  46. novelwriter/core/item.py +21 -3
  47. novelwriter/core/options.py +8 -7
  48. novelwriter/core/project.py +69 -44
  49. novelwriter/core/projectdata.py +1 -14
  50. novelwriter/core/projectxml.py +13 -41
  51. novelwriter/core/sessions.py +2 -1
  52. novelwriter/core/spellcheck.py +2 -1
  53. novelwriter/core/status.py +2 -1
  54. novelwriter/core/storage.py +178 -140
  55. novelwriter/core/tohtml.py +4 -2
  56. novelwriter/core/tokenizer.py +73 -45
  57. novelwriter/core/toodt.py +40 -30
  58. novelwriter/core/tree.py +3 -2
  59. novelwriter/dialogs/about.py +70 -160
  60. novelwriter/dialogs/docmerge.py +6 -5
  61. novelwriter/dialogs/docsplit.py +6 -6
  62. novelwriter/dialogs/editlabel.py +1 -1
  63. novelwriter/dialogs/preferences.py +553 -703
  64. novelwriter/dialogs/{projsettings.py → projectsettings.py} +288 -262
  65. novelwriter/dialogs/quotes.py +27 -23
  66. novelwriter/dialogs/wordlist.py +96 -40
  67. novelwriter/enum.py +20 -18
  68. novelwriter/error.py +1 -1
  69. novelwriter/extensions/circularprogress.py +11 -11
  70. novelwriter/extensions/configlayout.py +185 -134
  71. novelwriter/extensions/modified.py +81 -0
  72. novelwriter/extensions/novelselector.py +26 -12
  73. novelwriter/extensions/pagedsidebar.py +14 -16
  74. novelwriter/extensions/simpleprogress.py +5 -5
  75. novelwriter/extensions/statusled.py +8 -8
  76. novelwriter/extensions/switch.py +31 -63
  77. novelwriter/extensions/switchbox.py +1 -1
  78. novelwriter/extensions/versioninfo.py +153 -0
  79. novelwriter/gui/doceditor.py +178 -150
  80. novelwriter/gui/dochighlight.py +63 -92
  81. novelwriter/gui/docviewer.py +49 -51
  82. novelwriter/gui/docviewerpanel.py +72 -24
  83. novelwriter/gui/itemdetails.py +7 -7
  84. novelwriter/gui/mainmenu.py +14 -18
  85. novelwriter/gui/noveltree.py +9 -8
  86. novelwriter/gui/outline.py +98 -75
  87. novelwriter/gui/projtree.py +188 -61
  88. novelwriter/gui/sidebar.py +3 -4
  89. novelwriter/gui/statusbar.py +3 -4
  90. novelwriter/gui/theme.py +60 -68
  91. novelwriter/guimain.py +49 -156
  92. novelwriter/shared.py +15 -1
  93. novelwriter/tools/dictionaries.py +5 -6
  94. novelwriter/tools/manuscript.py +6 -6
  95. novelwriter/tools/manussettings.py +192 -221
  96. novelwriter/tools/noveldetails.py +525 -0
  97. novelwriter/tools/welcome.py +802 -0
  98. novelwriter/tools/writingstats.py +9 -9
  99. novelwriter/assets/images/wizard-back.jpg +0 -0
  100. novelwriter/assets/text/gplv3_en.htm +0 -641
  101. novelwriter/assets/text/release_notes.htm +0 -60
  102. novelwriter/dialogs/projdetails.py +0 -518
  103. novelwriter/dialogs/projload.py +0 -294
  104. novelwriter/dialogs/updates.py +0 -172
  105. novelwriter/extensions/pageddialog.py +0 -130
  106. novelwriter/tools/projwizard.py +0 -478
  107. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/LICENSE.md +0 -0
  108. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/WHEEL +0 -0
  109. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/entry_points.txt +0 -0
  110. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/top_level.txt +0 -0
@@ -34,11 +34,13 @@ from pathlib import Path
34
34
  from functools import partial
35
35
 
36
36
  from PyQt5.QtCore import QCoreApplication, QRegularExpression
37
- from novelwriter.core.index import processComment
38
37
 
39
38
  from novelwriter.enum import nwComment, nwItemLayout
40
39
  from novelwriter.common import formatTimeStamp, numberToRoman, checkInt
41
- from novelwriter.constants import nwHeadFmt, nwRegEx, nwShortcode, nwUnicode
40
+ from novelwriter.constants import (
41
+ nwHeadFmt, nwKeyWords, nwLabels, nwRegEx, nwShortcode, nwUnicode, trConst
42
+ )
43
+ from novelwriter.core.index import processComment
42
44
  from novelwriter.core.project import NWProject
43
45
 
44
46
  logger = logging.getLogger(__name__)
@@ -112,13 +114,15 @@ class Tokenizer(ABC):
112
114
 
113
115
  # Data Variables
114
116
  self._text = "" # The raw text to be tokenized
115
- self._nwItem = None # The NWItem associated with the handle
116
- self._tokens = [] # The list of the processed tokens
117
+ self._nwItem = None # The NWItem currently being processed
117
118
  self._result = "" # The result of the last document
118
119
 
119
120
  self._keepMarkdown = False # Whether to keep the markdown text
120
121
  self._allMarkdown = [] # The result novelWriter markdown of all documents
121
122
 
123
+ # Processed Tokens
124
+ self._tokens: list[tuple[int, int, str, list[tuple[int, int]], int]] = []
125
+
122
126
  # User Settings
123
127
  self._textFont = "Serif" # Output text font
124
128
  self._textSize = 11 # Output text size
@@ -194,12 +198,12 @@ class Tokenizer(ABC):
194
198
  ##
195
199
 
196
200
  @property
197
- def theResult(self) -> str:
201
+ def result(self) -> str:
198
202
  """The result of the build process."""
199
203
  return self._result
200
204
 
201
205
  @property
202
- def theMarkdown(self) -> list:
206
+ def allMarkdown(self) -> list:
203
207
  """The combined novelWriter Markdown text."""
204
208
  return self._allMarkdown
205
209
 
@@ -350,7 +354,7 @@ class Tokenizer(ABC):
350
354
  title = f"{trNotes}: {tItem.itemName}"
351
355
  self._tokens = []
352
356
  self._tokens.append((
353
- self.T_TITLE, 0, title, None, textAlign
357
+ self.T_TITLE, 0, title, [], textAlign
354
358
  ))
355
359
  if self._keepMarkdown:
356
360
  self._allMarkdown.append(f"# {title}\n\n")
@@ -358,8 +362,8 @@ class Tokenizer(ABC):
358
362
  return True
359
363
 
360
364
  def setText(self, tHandle: str, text: str | None = None) -> bool:
361
- """Set the text for the tokenizer from a handle. If theText is
362
- not set, load it from the file.
365
+ """Set the text for the tokenizer from a handle. If text is not
366
+ set, load it from the file.
363
367
  """
364
368
  self._nwItem = self._project.tree[tHandle]
365
369
  if self._nwItem is None:
@@ -418,7 +422,7 @@ class Tokenizer(ABC):
418
422
  # Check for blank lines
419
423
  if len(sLine) == 0:
420
424
  self._tokens.append((
421
- self.T_EMPTY, nHead, "", None, self.A_NONE
425
+ self.T_EMPTY, nHead, "", [], self.A_NONE
422
426
  ))
423
427
  if self._keepMarkdown:
424
428
  tmpMarkdown.append("\n")
@@ -446,7 +450,7 @@ class Tokenizer(ABC):
446
450
 
447
451
  elif sLine == "[vspace]":
448
452
  self._tokens.append(
449
- (self.T_SKIP, nHead, "", None, sAlign)
453
+ (self.T_SKIP, nHead, "", [], sAlign)
450
454
  )
451
455
  continue
452
456
 
@@ -454,38 +458,42 @@ class Tokenizer(ABC):
454
458
  nSkip = checkInt(sLine[8:-1], 0)
455
459
  if nSkip >= 1:
456
460
  self._tokens.append(
457
- (self.T_SKIP, nHead, "", None, sAlign)
461
+ (self.T_SKIP, nHead, "", [], sAlign)
458
462
  )
459
463
  if nSkip > 1:
460
464
  self._tokens += (nSkip - 1) * [
461
- (self.T_SKIP, nHead, "", None, self.A_NONE)
465
+ (self.T_SKIP, nHead, "", [], self.A_NONE)
462
466
  ]
463
467
  continue
464
468
 
465
469
  if aLine[0] == "%":
470
+ if aLine[1] == "~":
471
+ # Completely ignore the paragraph
472
+ continue
473
+
466
474
  cStyle, cText, _ = processComment(aLine)
467
475
  if cStyle == nwComment.SYNOPSIS:
468
476
  self._tokens.append((
469
- self.T_SYNOPSIS, nHead, cText, None, sAlign
477
+ self.T_SYNOPSIS, nHead, cText, [], sAlign
470
478
  ))
471
479
  if self._doSynopsis and self._keepMarkdown:
472
480
  tmpMarkdown.append("%s\n" % aLine)
473
481
  elif cStyle == nwComment.SHORT:
474
482
  self._tokens.append((
475
- self.T_SHORT, nHead, cText, None, sAlign
483
+ self.T_SHORT, nHead, cText, [], sAlign
476
484
  ))
477
485
  if self._doSynopsis and self._keepMarkdown:
478
486
  tmpMarkdown.append("%s\n" % aLine)
479
487
  else:
480
488
  self._tokens.append((
481
- self.T_COMMENT, nHead, cText, None, sAlign
489
+ self.T_COMMENT, nHead, cText, [], sAlign
482
490
  ))
483
491
  if self._doComments and self._keepMarkdown:
484
492
  tmpMarkdown.append("%s\n" % aLine)
485
493
 
486
494
  elif aLine[0] == "@":
487
495
  self._tokens.append((
488
- self.T_KEYWORD, nHead, aLine[1:].strip(), None, sAlign
496
+ self.T_KEYWORD, nHead, aLine[1:].strip(), [], sAlign
489
497
  ))
490
498
  if self._doKeywords and self._keepMarkdown:
491
499
  tmpMarkdown.append("%s\n" % aLine)
@@ -497,7 +505,7 @@ class Tokenizer(ABC):
497
505
 
498
506
  nHead += 1
499
507
  self._tokens.append((
500
- self.T_HEAD1, nHead, aLine[2:].strip(), None, sAlign
508
+ self.T_HEAD1, nHead, aLine[2:].strip(), [], sAlign
501
509
  ))
502
510
  if self._keepMarkdown:
503
511
  tmpMarkdown.append("%s\n" % aLine)
@@ -508,7 +516,7 @@ class Tokenizer(ABC):
508
516
 
509
517
  nHead += 1
510
518
  self._tokens.append((
511
- self.T_HEAD2, nHead, aLine[3:].strip(), None, sAlign
519
+ self.T_HEAD2, nHead, aLine[3:].strip(), [], sAlign
512
520
  ))
513
521
  if self._keepMarkdown:
514
522
  tmpMarkdown.append("%s\n" % aLine)
@@ -516,7 +524,7 @@ class Tokenizer(ABC):
516
524
  elif aLine[:4] == "### ":
517
525
  nHead += 1
518
526
  self._tokens.append((
519
- self.T_HEAD3, nHead, aLine[4:].strip(), None, sAlign
527
+ self.T_HEAD3, nHead, aLine[4:].strip(), [], sAlign
520
528
  ))
521
529
  if self._keepMarkdown:
522
530
  tmpMarkdown.append("%s\n" % aLine)
@@ -524,7 +532,7 @@ class Tokenizer(ABC):
524
532
  elif aLine[:5] == "#### ":
525
533
  nHead += 1
526
534
  self._tokens.append((
527
- self.T_HEAD4, nHead, aLine[5:].strip(), None, sAlign
535
+ self.T_HEAD4, nHead, aLine[5:].strip(), [], sAlign
528
536
  ))
529
537
  if self._keepMarkdown:
530
538
  tmpMarkdown.append("%s\n" % aLine)
@@ -537,7 +545,7 @@ class Tokenizer(ABC):
537
545
  tStyle = self.T_HEAD1
538
546
 
539
547
  self._tokens.append((
540
- tStyle, nHead, aLine[3:].strip(), None, sAlign | self.A_CENTRE
548
+ tStyle, nHead, aLine[3:].strip(), [], sAlign | self.A_CENTRE
541
549
  ))
542
550
  if self._keepMarkdown:
543
551
  tmpMarkdown.append("%s\n" % aLine)
@@ -551,7 +559,7 @@ class Tokenizer(ABC):
551
559
  tStyle = self.T_HEAD2
552
560
 
553
561
  self._tokens.append((
554
- tStyle, nHead, aLine[4:].strip(), None, sAlign
562
+ tStyle, nHead, aLine[4:].strip(), [], sAlign
555
563
  ))
556
564
  if self._keepMarkdown:
557
565
  tmpMarkdown.append("%s\n" % aLine)
@@ -614,7 +622,7 @@ class Tokenizer(ABC):
614
622
 
615
623
  # Always add an empty line at the end of the file
616
624
  self._tokens.append((
617
- self.T_EMPTY, nHead, "", None, self.A_NONE
625
+ self.T_EMPTY, nHead, "", [], self.A_NONE
618
626
  ))
619
627
  if self._keepMarkdown:
620
628
  tmpMarkdown.append("\n")
@@ -626,8 +634,8 @@ class Tokenizer(ABC):
626
634
  # ===========
627
635
  # Some items need a second pass
628
636
 
629
- pToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
630
- nToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
637
+ pToken = (self.T_EMPTY, 0, "", [], self.A_NONE)
638
+ nToken = (self.T_EMPTY, 0, "", [], self.A_NONE)
631
639
  tCount = len(self._tokens)
632
640
  for n, tToken in enumerate(self._tokens):
633
641
 
@@ -655,6 +663,8 @@ class Tokenizer(ABC):
655
663
  if not self._isNovel:
656
664
  return False
657
665
 
666
+ self._hFormatter.setHandle(self._nwItem.itemHandle if self._nwItem else None)
667
+
658
668
  for n, tToken in enumerate(self._tokens):
659
669
 
660
670
  # In case we see text before a scene, we reset the flag
@@ -664,9 +674,9 @@ class Tokenizer(ABC):
664
674
  elif tToken[0] == self.T_HEAD1:
665
675
  # Partition
666
676
 
667
- tTemp = self._hFormatter.apply(self._fmtTitle, tToken[2])
677
+ tTemp = self._hFormatter.apply(self._fmtTitle, tToken[2], tToken[1])
668
678
  self._tokens[n] = (
669
- tToken[0], tToken[1], tTemp, None, tToken[4]
679
+ tToken[0], tToken[1], tTemp, [], tToken[4]
670
680
  )
671
681
 
672
682
  elif tToken[0] in (self.T_HEAD2, self.T_UNNUM):
@@ -674,14 +684,14 @@ class Tokenizer(ABC):
674
684
 
675
685
  # Numbered or Unnumbered
676
686
  if tToken[0] == self.T_UNNUM:
677
- tTemp = self._hFormatter.apply(self._fmtUnNum, tToken[2])
687
+ tTemp = self._hFormatter.apply(self._fmtUnNum, tToken[2], tToken[1])
678
688
  else:
679
689
  self._hFormatter.incChapter()
680
- tTemp = self._hFormatter.apply(self._fmtChapter, tToken[2])
690
+ tTemp = self._hFormatter.apply(self._fmtChapter, tToken[2], tToken[1])
681
691
 
682
692
  # Format the chapter header
683
693
  self._tokens[n] = (
684
- tToken[0], tToken[1], tTemp, None, tToken[4]
694
+ tToken[0], tToken[1], tTemp, [], tToken[4]
685
695
  )
686
696
 
687
697
  # Set scene variables
@@ -693,32 +703,32 @@ class Tokenizer(ABC):
693
703
 
694
704
  self._hFormatter.incScene()
695
705
 
696
- tTemp = self._hFormatter.apply(self._fmtScene, tToken[2])
706
+ tTemp = self._hFormatter.apply(self._fmtScene, tToken[2], tToken[1])
697
707
  if tTemp == "" and self._hideScene:
698
708
  self._tokens[n] = (
699
- self.T_EMPTY, tToken[1], "", None, self.A_NONE
709
+ self.T_EMPTY, tToken[1], "", [], self.A_NONE
700
710
  )
701
711
  elif tTemp == "" and not self._hideScene:
702
712
  if self._firstScene:
703
713
  self._tokens[n] = (
704
- self.T_EMPTY, tToken[1], "", None, self.A_NONE
714
+ self.T_EMPTY, tToken[1], "", [], self.A_NONE
705
715
  )
706
716
  else:
707
717
  self._tokens[n] = (
708
- self.T_SKIP, tToken[1], "", None, tToken[4]
718
+ self.T_SKIP, tToken[1], "", [], tToken[4]
709
719
  )
710
720
  elif tTemp == self._fmtScene:
711
721
  if self._firstScene:
712
722
  self._tokens[n] = (
713
- self.T_EMPTY, tToken[1], "", None, self.A_NONE
723
+ self.T_EMPTY, tToken[1], "", [], self.A_NONE
714
724
  )
715
725
  else:
716
726
  self._tokens[n] = (
717
- self.T_SEP, tToken[1], tTemp, None, tToken[4] | self.A_CENTRE
727
+ self.T_SEP, tToken[1], tTemp, [], tToken[4] | self.A_CENTRE
718
728
  )
719
729
  else:
720
730
  self._tokens[n] = (
721
- tToken[0], tToken[1], tTemp, None, tToken[4]
731
+ tToken[0], tToken[1], tTemp, [], tToken[4]
722
732
  )
723
733
 
724
734
  self._firstScene = False
@@ -726,22 +736,22 @@ class Tokenizer(ABC):
726
736
  elif tToken[0] == self.T_HEAD4:
727
737
  # Section
728
738
 
729
- tTemp = self._hFormatter.apply(self._fmtSection, tToken[2])
739
+ tTemp = self._hFormatter.apply(self._fmtSection, tToken[2], tToken[1])
730
740
  if tTemp == "" and self._hideSection:
731
741
  self._tokens[n] = (
732
- self.T_EMPTY, tToken[1], "", None, self.A_NONE
742
+ self.T_EMPTY, tToken[1], "", [], self.A_NONE
733
743
  )
734
744
  elif tTemp == "" and not self._hideSection:
735
745
  self._tokens[n] = (
736
- self.T_SKIP, tToken[1], "", None, tToken[4]
746
+ self.T_SKIP, tToken[1], "", [], tToken[4]
737
747
  )
738
748
  elif tTemp == self._fmtSection:
739
749
  self._tokens[n] = (
740
- self.T_SEP, tToken[1], tTemp, None, tToken[4] | self.A_CENTRE
750
+ self.T_SEP, tToken[1], tTemp, [], tToken[4] | self.A_CENTRE
741
751
  )
742
752
  else:
743
753
  self._tokens[n] = (
744
- tToken[0], tToken[1], tTemp, None, tToken[4]
754
+ tToken[0], tToken[1], tTemp, [], tToken[4]
745
755
  )
746
756
 
747
757
  return True
@@ -759,7 +769,6 @@ class Tokenizer(ABC):
759
769
  data = {
760
770
  "meta": {
761
771
  "projectName": self._project.data.name,
762
- "novelTitle": self._project.data.title,
763
772
  "novelAuthor": self._project.data.author,
764
773
  "buildTime": int(timeStamp),
765
774
  "buildTimeStr": formatTimeStamp(timeStamp),
@@ -818,11 +827,17 @@ class HeadingFormatter:
818
827
 
819
828
  def __init__(self, project: NWProject) -> None:
820
829
  self._project = project
830
+ self._handle = None
821
831
  self._chCount = 0
822
832
  self._scChCount = 0
823
833
  self._scAbsCount = 0
824
834
  return
825
835
 
836
+ def setHandle(self, tHandle: str | None) -> None:
837
+ """Set the handle currently being processed."""
838
+ self._handle = tHandle
839
+ return
840
+
826
841
  def incChapter(self) -> None:
827
842
  """Increment the chapter counter."""
828
843
  self._chCount += 1
@@ -839,7 +854,7 @@ class HeadingFormatter:
839
854
  self._scChCount = 0
840
855
  return
841
856
 
842
- def apply(self, hFormat: str, text: str) -> str:
857
+ def apply(self, hFormat: str, text: str, nHead: int) -> str:
843
858
  """Apply formatting to a specific heading."""
844
859
  hFormat = hFormat.replace(nwHeadFmt.TITLE, text)
845
860
  hFormat = hFormat.replace(nwHeadFmt.CH_NUM, str(self._chCount))
@@ -855,6 +870,19 @@ class HeadingFormatter:
855
870
  chRom = numberToRoman(self._chCount, toLower=False)
856
871
  hFormat = hFormat.replace(nwHeadFmt.CH_ROMU, chRom)
857
872
 
873
+ if nwHeadFmt.CHAR_POV in hFormat or nwHeadFmt.CHAR_FOCUS in hFormat:
874
+ if self._handle and nHead > 0:
875
+ index = self._project.index
876
+ pList = index.getReferenceForHeader(self._handle, nHead, nwKeyWords.POV_KEY)
877
+ fList = index.getReferenceForHeader(self._handle, nHead, nwKeyWords.FOCUS_KEY)
878
+ pText = pList[0] if pList else nwUnicode.U_ENDASH
879
+ fText = fList[0] if fList else nwUnicode.U_ENDASH
880
+ else:
881
+ pText = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
882
+ fText = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY])
883
+ hFormat = hFormat.replace(nwHeadFmt.CHAR_POV, pText)
884
+ hFormat = hFormat.replace(nwHeadFmt.CHAR_FOCUS, fText)
885
+
858
886
  return hFormat
859
887
 
860
888
  # END Class HeadingFormatter
novelwriter/core/toodt.py CHANGED
@@ -29,11 +29,11 @@ from __future__ import annotations
29
29
  import logging
30
30
  import xml.etree.ElementTree as ET
31
31
 
32
- from typing import Sequence
33
32
  from hashlib import sha256
34
33
  from pathlib import Path
35
34
  from zipfile import ZipFile
36
35
  from datetime import datetime
36
+ from collections.abc import Sequence
37
37
 
38
38
  from novelwriter import __version__
39
39
  from novelwriter.common import xmlIndent
@@ -132,11 +132,12 @@ class ToOdt(Tokenizer):
132
132
  self._errData = [] # List of errors encountered
133
133
 
134
134
  # Properties
135
- self._textFont = "Liberation Serif"
136
- self._textSize = 12
137
- self._textFixed = False
138
- self._colourHead = False
139
- self._headerText = ""
135
+ self._textFont = "Liberation Serif"
136
+ self._textSize = 12
137
+ self._textFixed = False
138
+ self._colourHead = False
139
+ self._headerFormat = ""
140
+ self._pageOffset = 0
140
141
 
141
142
  # Internal
142
143
  self._fontFamily = "'Liberation Serif'"
@@ -222,6 +223,12 @@ class ToOdt(Tokenizer):
222
223
  self._mDocRight = f"{right/10.0:.3f}cm"
223
224
  return
224
225
 
226
+ def setHeaderFormat(self, format: str, offset: int) -> None:
227
+ """Set the document header format."""
228
+ self._headerFormat = format.strip()
229
+ self._pageOffset = offset
230
+ return
231
+
225
232
  ##
226
233
  # Class Methods
227
234
  ##
@@ -279,14 +286,6 @@ class ToOdt(Tokenizer):
279
286
  # Clear Errors
280
287
  self._errData = []
281
288
 
282
- # Document Header
283
- # ===============
284
-
285
- if self._headerText == "":
286
- theTitle = self._project.data.title or self._project.data.name
287
- theAuth = self._project.data.author
288
- self._headerText = f"{theTitle} / {theAuth} /"
289
-
290
289
  # Create Roots
291
290
  # ============
292
291
 
@@ -373,7 +372,7 @@ class ToOdt(Tokenizer):
373
372
 
374
373
  # Dublin Core Meta Data
375
374
  xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "title"))
376
- xMeta.text = self._project.data.title or self._project.data.name
375
+ xMeta.text = self._project.data.name
377
376
 
378
377
  xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "date"))
379
378
  xMeta.text = timeStamp
@@ -684,12 +683,12 @@ class ToOdt(Tokenizer):
684
683
  return parName
685
684
 
686
685
  oStyle.setParentStyleName(parName)
687
- theID = oStyle.getID()
688
- if theID in self._autoPara:
689
- return self._autoPara[theID][0]
686
+ pID = oStyle.getID()
687
+ if pID in self._autoPara:
688
+ return self._autoPara[pID][0]
690
689
 
691
690
  newName = "P%d" % (len(self._autoPara) + 1)
692
- self._autoPara[theID] = (newName, oStyle)
691
+ self._autoPara[pID] = (newName, oStyle)
693
692
 
694
693
  return newName
695
694
 
@@ -1012,17 +1011,28 @@ class ToOdt(Tokenizer):
1012
1011
  xPage = ET.SubElement(self._xMast, _mkTag("style", "master-page"), attrib=tAttr)
1013
1012
 
1014
1013
  # Standard Page Header
1015
- xHead = ET.SubElement(xPage, _mkTag("style", "header"))
1016
- xPar = ET.SubElement(xHead, _mkTag("text", "p"), attrib={
1017
- _mkTag("text", "style-name"): "Header"
1018
- })
1019
- xPar.text = self._headerText.strip() + " "
1020
-
1021
- xTail = ET.SubElement(xPar, _mkTag("text", "page-number"), attrib={
1022
- _mkTag("text", "select-page"): "current"
1023
- })
1024
- xTail.text = "2"
1025
- xTail.tail = "" # Prevent line break in indented XML
1014
+ if self._headerFormat:
1015
+ pre, page, post = self._headerFormat.partition(nwHeadFmt.ODT_PAGE)
1016
+
1017
+ pre = pre.replace(nwHeadFmt.ODT_PROJECT, self._project.data.name)
1018
+ pre = pre.replace(nwHeadFmt.ODT_AUTHOR, self._project.data.author)
1019
+ post = post.replace(nwHeadFmt.ODT_PROJECT, self._project.data.name)
1020
+ post = post.replace(nwHeadFmt.ODT_AUTHOR, self._project.data.author)
1021
+
1022
+ xHead = ET.SubElement(xPage, _mkTag("style", "header"))
1023
+ xPar = ET.SubElement(xHead, _mkTag("text", "p"), attrib={
1024
+ _mkTag("text", "style-name"): "Header"
1025
+ })
1026
+ xPar.text = pre
1027
+ if page:
1028
+ attrib = {_mkTag("text", "select-page"): "current"}
1029
+ if self._pageOffset > 0:
1030
+ attrib = {_mkTag("text", "page-adjust"): str(0 - self._pageOffset)}
1031
+ xTail = ET.SubElement(xPar, _mkTag("text", "page-number"), attrib=attrib)
1032
+ xTail.text = "2"
1033
+ xTail.tail = post
1034
+ else:
1035
+ xPar.text += post
1026
1036
 
1027
1037
  # First Page Header
1028
1038
  xHead = ET.SubElement(xPage, _mkTag("style", "header-first"))
novelwriter/core/tree.py CHANGED
@@ -26,8 +26,9 @@ from __future__ import annotations
26
26
  import random
27
27
  import logging
28
28
 
29
- from typing import TYPE_CHECKING, Iterator, Literal, overload
29
+ from typing import TYPE_CHECKING, Literal, overload
30
30
  from pathlib import Path
31
+ from collections.abc import Iterator
31
32
 
32
33
  from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
33
34
  from novelwriter.error import logException
@@ -500,7 +501,7 @@ class NWTree:
500
501
  ##
501
502
 
502
503
  def _setTreeChanged(self, state: bool) -> None:
503
- """Set the changed flag to theState, and if being set to True,
504
+ """Set the changed flag to state, and if being set to True,
504
505
  propagate that state change to the parent NWProject class.
505
506
  """
506
507
  self._changed = state