novelWriter 2.6rc1__py3-none-any.whl → 2.6.2__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 (62) hide show
  1. {novelWriter-2.6rc1.dist-info → novelWriter-2.6.2.dist-info}/METADATA +1 -1
  2. {novelWriter-2.6rc1.dist-info → novelWriter-2.6.2.dist-info}/RECORD +62 -59
  3. novelwriter/__init__.py +3 -3
  4. novelwriter/assets/i18n/nw_cs_CZ.qm +0 -0
  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_ru_RU.qm +0 -0
  16. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  17. novelwriter/assets/i18n/project_cs_CZ.json +118 -0
  18. novelwriter/assets/i18n/project_de_DE.json +2 -0
  19. novelwriter/assets/i18n/project_en_US.json +2 -0
  20. novelwriter/assets/i18n/project_es_419.json +2 -0
  21. novelwriter/assets/i18n/project_fr_FR.json +3 -1
  22. novelwriter/assets/i18n/project_it_IT.json +2 -0
  23. novelwriter/assets/i18n/project_ja_JP.json +2 -0
  24. novelwriter/assets/i18n/project_nb_NO.json +2 -0
  25. novelwriter/assets/i18n/project_nl_NL.json +2 -0
  26. novelwriter/assets/i18n/project_pl_PL.json +2 -0
  27. novelwriter/assets/i18n/project_pt_BR.json +2 -0
  28. novelwriter/assets/i18n/project_zh_CN.json +2 -0
  29. novelwriter/assets/manual.pdf +0 -0
  30. novelwriter/assets/manual_fr_FR.pdf +0 -0
  31. novelwriter/assets/sample.zip +0 -0
  32. novelwriter/assets/text/credits_en.htm +1 -0
  33. novelwriter/config.py +41 -19
  34. novelwriter/constants.py +4 -0
  35. novelwriter/core/buildsettings.py +7 -0
  36. novelwriter/core/itemmodel.py +4 -2
  37. novelwriter/core/project.py +2 -6
  38. novelwriter/dialogs/docsplit.py +49 -45
  39. novelwriter/dialogs/preferences.py +14 -5
  40. novelwriter/enum.py +0 -7
  41. novelwriter/extensions/novelselector.py +3 -2
  42. novelwriter/extensions/statusled.py +5 -6
  43. novelwriter/formats/shared.py +12 -11
  44. novelwriter/formats/todocx.py +1 -1
  45. novelwriter/formats/tohtml.py +30 -27
  46. novelwriter/formats/tokenizer.py +14 -8
  47. novelwriter/formats/tomarkdown.py +3 -2
  48. novelwriter/formats/toodt.py +1 -1
  49. novelwriter/formats/toqdoc.py +2 -0
  50. novelwriter/gui/doceditor.py +35 -19
  51. novelwriter/gui/docviewer.py +1 -1
  52. novelwriter/gui/itemdetails.py +1 -0
  53. novelwriter/gui/projtree.py +16 -18
  54. novelwriter/gui/statusbar.py +6 -7
  55. novelwriter/guimain.py +1 -1
  56. novelwriter/tools/dictionaries.py +2 -2
  57. novelwriter/tools/manussettings.py +8 -8
  58. novelwriter/tools/welcome.py +1 -1
  59. {novelWriter-2.6rc1.dist-info → novelWriter-2.6.2.dist-info}/LICENSE.md +0 -0
  60. {novelWriter-2.6rc1.dist-info → novelWriter-2.6.2.dist-info}/WHEEL +0 -0
  61. {novelWriter-2.6rc1.dist-info → novelWriter-2.6.2.dist-info}/entry_points.txt +0 -0
  62. {novelWriter-2.6rc1.dist-info → novelWriter-2.6.2.dist-info}/top_level.txt +0 -0
novelwriter/config.py CHANGED
@@ -32,6 +32,7 @@ import sys
32
32
  from datetime import datetime
33
33
  from pathlib import Path
34
34
  from time import time
35
+ from typing import TYPE_CHECKING
35
36
 
36
37
  from PyQt5.QtCore import (
37
38
  PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QLibraryInfo,
@@ -47,6 +48,9 @@ from novelwriter.common import (
47
48
  from novelwriter.constants import nwFiles, nwUnicode
48
49
  from novelwriter.error import formatException, logException
49
50
 
51
+ if TYPE_CHECKING: # pragma: no cover
52
+ from novelwriter.core.projectdata import NWProjectData
53
+
50
54
  logger = logging.getLogger(__name__)
51
55
 
52
56
 
@@ -101,8 +105,11 @@ class Config:
101
105
  self._qtTrans = {}
102
106
 
103
107
  # PDF Manual
104
- pdfDocs = self._appPath / "assets" / "manual.pdf"
105
- self.pdfDocs = pdfDocs if pdfDocs.is_file() else None
108
+ self._manuals: dict[str, Path] = {}
109
+ if (assets := self._appPath / "assets").is_dir():
110
+ for item in assets.iterdir():
111
+ if item.is_file() and item.stem.startswith("manual") and item.suffix == ".pdf":
112
+ self._manuals[item.stem] = item
106
113
 
107
114
  # User Settings
108
115
  # =============
@@ -112,7 +119,7 @@ class Config:
112
119
 
113
120
  # General GUI Settings
114
121
  self.guiLocale = self._qLocale.name()
115
- self.guiTheme = "default" # GUI theme
122
+ self.guiTheme = "default_light" # GUI theme
116
123
  self.guiSyntax = "default_light" # Syntax theme
117
124
  self.guiFont = QFont() # Main GUI font
118
125
  self.guiScale = 1.0 # Set automatically by Theme class
@@ -135,6 +142,7 @@ class Config:
135
142
  self.emphLabels = True # Add emphasis to H1 and H2 item labels
136
143
  self.backupOnClose = False # Flag for running automatic backups
137
144
  self.askBeforeBackup = True # Flag for asking before running automatic backup
145
+ self.askBeforeExit = True # Flag for asking before exiting the app
138
146
 
139
147
  # Text Editor Settings
140
148
  self.textFont = QFont() # Editor font
@@ -257,6 +265,10 @@ class Config:
257
265
  def hasError(self) -> bool:
258
266
  return self._hasError
259
267
 
268
+ @property
269
+ def pdfDocs(self) -> Path | None:
270
+ return self._manuals.get(f"manual_{self.locale.name()}", self._manuals.get("manual"))
271
+
260
272
  @property
261
273
  def locale(self) -> QLocale:
262
274
  return self._dLocale
@@ -628,6 +640,7 @@ class Config:
628
640
  self._backupPath = conf.rdPath(sec, "backuppath", self._backupPath)
629
641
  self.backupOnClose = conf.rdBool(sec, "backuponclose", self.backupOnClose)
630
642
  self.askBeforeBackup = conf.rdBool(sec, "askbeforebackup", self.askBeforeBackup)
643
+ self.askBeforeExit = conf.rdBool(sec, "askbeforeexit", self.askBeforeExit)
631
644
 
632
645
  # Editor
633
646
  sec = "Editor"
@@ -739,6 +752,7 @@ class Config:
739
752
  "backuppath": str(self._backupPath),
740
753
  "backuponclose": str(self.backupOnClose),
741
754
  "askbeforebackup": str(self.askBeforeBackup),
755
+ "askbeforeexit": str(self.askBeforeExit),
742
756
  }
743
757
 
744
758
  conf["Editor"] = {
@@ -842,29 +856,30 @@ class RecentProjects:
842
856
 
843
857
  def __init__(self, config: Config) -> None:
844
858
  self._conf = config
845
- self._data = {}
859
+ self._data: dict[str, dict[str, str | int]] = {}
860
+ self._map: dict[str, str] = {}
846
861
  return
847
862
 
848
863
  def loadCache(self) -> bool:
849
864
  """Load the cache file for recent projects."""
850
865
  self._data = {}
851
-
866
+ self._map = {}
852
867
  cacheFile = self._conf.dataPath(nwFiles.RECENT_FILE)
853
868
  if cacheFile.is_file():
854
869
  try:
855
870
  with open(cacheFile, mode="r", encoding="utf-8") as inFile:
856
871
  data = json.load(inFile)
857
872
  for path, entry in data.items():
858
- self._data[path] = {
859
- "title": entry.get("title", ""),
860
- "words": entry.get("words", 0),
861
- "time": entry.get("time", 0),
862
- }
873
+ puuid = str(entry.get("uuid", ""))
874
+ title = str(entry.get("title", ""))
875
+ words = checkInt(entry.get("words", 0), 0)
876
+ saved = checkInt(entry.get("time", 0), 0)
877
+ if path and title:
878
+ self._setEntry(puuid, path, title, words, saved)
863
879
  except Exception:
864
880
  logger.error("Could not load recent project cache")
865
881
  logException()
866
882
  return False
867
-
868
883
  return True
869
884
 
870
885
  def saveCache(self) -> bool:
@@ -879,7 +894,6 @@ class RecentProjects:
879
894
  logger.error("Could not save recent project cache")
880
895
  logException()
881
896
  return False
882
-
883
897
  return True
884
898
 
885
899
  def listEntries(self) -> list[tuple[str, str, int, int]]:
@@ -889,14 +903,15 @@ class RecentProjects:
889
903
  for k, e in self._data.items()
890
904
  ]
891
905
 
892
- def update(self, path: str | Path, title: str, words: int, saved: float | int) -> None:
906
+ def update(self, path: str | Path, data: NWProjectData, saved: float | int) -> None:
893
907
  """Add or update recent cache information on a given project."""
894
- self._data[str(path)] = {
895
- "title": title,
896
- "words": int(words),
897
- "time": int(saved),
898
- }
899
- self.saveCache()
908
+ try:
909
+ if (remove := self._map.get(data.uuid)) and (remove != str(path)):
910
+ self.remove(remove)
911
+ self._setEntry(data.uuid, str(path), data.name, sum(data.currCounts), int(saved))
912
+ self.saveCache()
913
+ except Exception:
914
+ pass
900
915
  return
901
916
 
902
917
  def remove(self, path: str | Path) -> None:
@@ -906,6 +921,13 @@ class RecentProjects:
906
921
  self.saveCache()
907
922
  return
908
923
 
924
+ def _setEntry(self, puuid: str, path: str, title: str, words: int, saved: int) -> None:
925
+ """Set an entry in the recent projects record."""
926
+ self._data[path] = {"uuid": puuid, "title": title, "words": words, "time": saved}
927
+ if puuid:
928
+ self._map[puuid] = path
929
+ return
930
+
909
931
 
910
932
  class RecentPaths:
911
933
 
novelwriter/constants.py CHANGED
@@ -173,6 +173,10 @@ class nwKeyWords:
173
173
  TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
174
174
  OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY, STORY_KEY, MENTION_KEY,
175
175
  ]
176
+ CAN_CREATE = [
177
+ POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
178
+ OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY,
179
+ ]
176
180
 
177
181
  # Set of Valid Keys
178
182
  VALID_KEYS = set(ALL_KEYS)
@@ -159,6 +159,13 @@ SETTINGS_LABELS = {
159
159
  "format.indentFirstPar": QT_TRANSLATE_NOOP("Builds", "Indent First Paragraph"),
160
160
 
161
161
  "format.grpMargins": QT_TRANSLATE_NOOP("Builds", "Text Margins"),
162
+ "format.titleMargin": QT_TRANSLATE_NOOP("Builds", "Title and Partition"),
163
+ "format.h1Margin": QT_TRANSLATE_NOOP("Builds", "Heading 1 and Chapter"),
164
+ "format.h2Margin": QT_TRANSLATE_NOOP("Builds", "Heading 2 and Scene"),
165
+ "format.h3Margin": QT_TRANSLATE_NOOP("Builds", "Heading 3 and Section"),
166
+ "format.h4Margin": QT_TRANSLATE_NOOP("Builds", "Heading 4"),
167
+ "format.textMargin": QT_TRANSLATE_NOOP("Builds", "Text Paragraph"),
168
+ "format.sepMargin": QT_TRANSLATE_NOOP("Builds", "Scene Separator"),
162
169
 
163
170
  "format.grpPage": QT_TRANSLATE_NOOP("Builds", "Page Layout"),
164
171
  "format.pageUnit": QT_TRANSLATE_NOOP("Builds", "Unit"),
@@ -328,7 +328,7 @@ class ProjectModel(QAbstractItemModel):
328
328
 
329
329
  def parent(self, index: QModelIndex) -> QModelIndex:
330
330
  """Get the parent model index of another index."""
331
- if index.isValid() and (parent := index.internalPointer().parent()):
331
+ if index.isValid() and (node := index.internalPointer()) and (parent := node.parent()):
332
332
  return self.createIndex(parent.row(), 0, parent)
333
333
  return QModelIndex()
334
334
 
@@ -379,7 +379,9 @@ class ProjectModel(QAbstractItemModel):
379
379
  row: int, column: int, parent: QModelIndex
380
380
  ) -> bool:
381
381
  """Check if mime data can be dropped on the current location."""
382
- return data.hasFormat(nwConst.MIME_HANDLE) and action == Qt.DropAction.MoveAction
382
+ if parent.isValid() and parent.internalPointer() is not self._root:
383
+ return data.hasFormat(nwConst.MIME_HANDLE) and action == Qt.DropAction.MoveAction
384
+ return False
383
385
 
384
386
  def dropMimeData(
385
387
  self, data: QMimeData, action: Qt.DropAction,
@@ -353,9 +353,7 @@ class NWProject:
353
353
 
354
354
  # Update recent projects
355
355
  if storePath := self._storage.storagePath:
356
- CONFIG.recentProjects.update(
357
- storePath, self._data.name, sum(self._data.initCounts), time()
358
- )
356
+ CONFIG.recentProjects.update(storePath, self._data, time())
359
357
 
360
358
  # Check the project tree consistency
361
359
  # This also handles any orphaned files found
@@ -421,9 +419,7 @@ class NWProject:
421
419
 
422
420
  # Update recent projects
423
421
  if storagePath := self._storage.storagePath:
424
- CONFIG.recentProjects.update(
425
- storagePath, self._data.name, sum(self._data.currCounts), saveTime
426
- )
422
+ CONFIG.recentProjects.update(storagePath, self._data, saveTime)
427
423
 
428
424
  SHARED.newStatusMessage(self.tr("Saved Project: {0}").format(self._data.name))
429
425
  self.setProjectChanged(False)
@@ -71,10 +71,10 @@ class GuiDocSplit(NDialog):
71
71
  vSp = CONFIG.pxInt(8)
72
72
  bSp = CONFIG.pxInt(12)
73
73
 
74
- pOptions = SHARED.project.options
75
- spLevel = pOptions.getInt("GuiDocSplit", "spLevel", 3)
76
- intoFolder = pOptions.getBool("GuiDocSplit", "intoFolder", True)
77
- docHierarchy = pOptions.getBool("GuiDocSplit", "docHierarchy", True)
74
+ options = SHARED.project.options
75
+ spLevel = options.getInt("GuiDocSplit", "spLevel", 3)
76
+ intoFolder = options.getBool("GuiDocSplit", "intoFolder", True)
77
+ docHierarchy = options.getBool("GuiDocSplit", "docHierarchy", True)
78
78
 
79
79
  # Heading Selection
80
80
  self.listBox = QListWidget(self)
@@ -171,10 +171,10 @@ class GuiDocSplit(NDialog):
171
171
  self._data["moveToTrash"] = moveToTrash
172
172
 
173
173
  logger.debug("Saving State: GuiDocSplit")
174
- pOptions = SHARED.project.options
175
- pOptions.setValue("GuiDocSplit", "spLevel", spLevel)
176
- pOptions.setValue("GuiDocSplit", "intoFolder", intoFolder)
177
- pOptions.setValue("GuiDocSplit", "docHierarchy", docHierarchy)
174
+ options = SHARED.project.options
175
+ options.setValue("GuiDocSplit", "spLevel", spLevel)
176
+ options.setValue("GuiDocSplit", "intoFolder", intoFolder)
177
+ options.setValue("GuiDocSplit", "docHierarchy", docHierarchy)
178
178
 
179
179
  return self._data, self._text
180
180
 
@@ -218,42 +218,46 @@ class GuiDocSplit(NDialog):
218
218
  if not self._text:
219
219
  self._text = SHARED.project.storage.getDocumentText(sHandle).splitlines()
220
220
 
221
- for lineNo, aLine in enumerate(self._text):
222
-
223
- onLine = -1
224
- hLevel = 0
225
- hLabel = aLine.strip()
226
- if aLine.startswith("# ") and spLevel >= 1:
227
- onLine = lineNo
228
- hLevel = 1
229
- hLabel = aLine[2:].strip()
230
- elif aLine.startswith("## ") and spLevel >= 2:
231
- onLine = lineNo
232
- hLevel = 2
233
- hLabel = aLine[3:].strip()
234
- elif aLine.startswith("### ") and spLevel >= 3:
235
- onLine = lineNo
236
- hLevel = 3
237
- hLabel = aLine[4:].strip()
238
- elif aLine.startswith("#### ") and spLevel >= 4:
239
- onLine = lineNo
240
- hLevel = 4
241
- hLabel = aLine[5:].strip()
242
- elif aLine.startswith("#! ") and spLevel >= 1:
243
- onLine = lineNo
244
- hLevel = 1
245
- hLabel = aLine[3:].strip()
246
- elif aLine.startswith("##! ") and spLevel >= 2:
247
- onLine = lineNo
248
- hLevel = 2
249
- hLabel = aLine[4:].strip()
250
-
251
- if onLine >= 0 and hLevel > 0:
252
- newItem = QListWidgetItem()
253
- newItem.setText(aLine.strip())
254
- newItem.setData(self.LINE_ROLE, onLine)
255
- newItem.setData(self.LEVEL_ROLE, hLevel)
256
- newItem.setData(self.LABEL_ROLE, hLabel)
257
- self.listBox.addItem(newItem)
221
+ for i, line in enumerate(self._text):
222
+
223
+ pos = -1
224
+ level = 0
225
+ label = line.strip()
226
+ if line.startswith("# ") and spLevel >= 1:
227
+ pos = i
228
+ level = 1
229
+ label = line[2:].strip()
230
+ elif line.startswith("## ") and spLevel >= 2:
231
+ pos = i
232
+ level = 2
233
+ label = line[3:].strip()
234
+ elif line.startswith("### ") and spLevel >= 3:
235
+ pos = i
236
+ level = 3
237
+ label = line[4:].strip()
238
+ elif line.startswith("#### ") and spLevel >= 4:
239
+ pos = i
240
+ level = 4
241
+ label = line[5:].strip()
242
+ elif line.startswith("#! ") and spLevel >= 1:
243
+ pos = i
244
+ level = 1
245
+ label = line[3:].strip()
246
+ elif line.startswith("##! ") and spLevel >= 2:
247
+ pos = i
248
+ level = 2
249
+ label = line[4:].strip()
250
+ elif line.startswith("###! ") and spLevel >= 3:
251
+ pos = i
252
+ level = 3
253
+ label = line[5:].strip()
254
+
255
+ if pos >= 0 and level > 0:
256
+ trItem = QListWidgetItem()
257
+ trItem.setText(line.strip())
258
+ trItem.setData(self.LINE_ROLE, pos)
259
+ trItem.setData(self.LEVEL_ROLE, level)
260
+ trItem.setData(self.LABEL_ROLE, label)
261
+ self.listBox.addItem(trItem)
258
262
 
259
263
  return
@@ -271,10 +271,10 @@ class GuiPreferences(NDialog):
271
271
  self.tr("Include project notes in status bar word count"), self.incNotesWCount
272
272
  )
273
273
 
274
- # Auto Save
274
+ # Behaviour
275
275
  # =========
276
276
 
277
- title = self.tr("Auto Save")
277
+ title = self.tr("Behaviour")
278
278
  section += 1
279
279
  self.sidebar.addButton(title, section)
280
280
  self.mainForm.addGroupLabel(title, section)
@@ -301,6 +301,14 @@ class GuiPreferences(NDialog):
301
301
  self.tr("How often the project is automatically saved."), unit=self.tr("seconds")
302
302
  )
303
303
 
304
+ # Ask before exiting novelWriter
305
+ self.askBeforeExit = NSwitch(self)
306
+ self.askBeforeExit.setChecked(CONFIG.askBeforeExit)
307
+ self.mainForm.addRow(
308
+ self.tr("Ask before exiting novelWriter"), self.askBeforeExit,
309
+ self.tr("Only applies when a project is open.")
310
+ )
311
+
304
312
  # Project Backup
305
313
  # ==============
306
314
 
@@ -927,9 +935,10 @@ class GuiPreferences(NDialog):
927
935
  CONFIG.incNotesWCount = self.incNotesWCount.isChecked()
928
936
  CONFIG.setTextFont(self._textFont)
929
937
 
930
- # Auto Save
931
- CONFIG.autoSaveDoc = self.autoSaveDoc.value()
932
- CONFIG.autoSaveProj = self.autoSaveProj.value()
938
+ # Behaviour
939
+ CONFIG.autoSaveDoc = self.autoSaveDoc.value()
940
+ CONFIG.autoSaveProj = self.autoSaveProj.value()
941
+ CONFIG.askBeforeExit = self.askBeforeExit.isChecked()
933
942
 
934
943
  # Project Backup
935
944
  CONFIG.setBackupPath(self.backupPath)
novelwriter/enum.py CHANGED
@@ -68,13 +68,6 @@ class nwComment(Enum):
68
68
  STORY = 7
69
69
 
70
70
 
71
- class nwTrinary(Enum):
72
-
73
- NEGATIVE = -1
74
- NEUTRAL = 0
75
- POSITIVE = 1
76
-
77
-
78
71
  class nwChange(Enum):
79
72
 
80
73
  CREATE = 0
@@ -90,12 +90,13 @@ class NovelSelector(QComboBox):
90
90
  @pyqtSlot()
91
91
  def refreshNovelList(self) -> None:
92
92
  """Rebuild the list of novel items."""
93
+ cHandle = self.currentData()
94
+
93
95
  self._blockSignal = True
94
96
  self._firstHandle = None
95
97
  self.clear()
96
98
 
97
99
  icon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])
98
- handle = self.currentData()
99
100
  for tHandle, nwItem in SHARED.project.tree.iterRoots(nwItemClass.NOVEL):
100
101
  if self._listFormat:
101
102
  name = self._listFormat.format(nwItem.itemName)
@@ -110,7 +111,7 @@ class NovelSelector(QComboBox):
110
111
  self.insertSeparator(self.count())
111
112
  self.addItem(icon, self.tr("All Novel Folders"), "")
112
113
 
113
- self.setHandle(handle)
114
+ self.setHandle(cHandle)
114
115
  self.setEnabled(self.count() > 1)
115
116
  self._blockSignal = False
116
117
 
@@ -29,7 +29,6 @@ from PyQt5.QtGui import QColor, QPainter, QPaintEvent
29
29
  from PyQt5.QtWidgets import QAbstractButton, QWidget
30
30
 
31
31
  from novelwriter import CONFIG
32
- from novelwriter.enum import nwTrinary
33
32
  from novelwriter.types import QtBlack, QtPaintAntiAlias
34
33
 
35
34
  logger = logging.getLogger(__name__)
@@ -47,14 +46,14 @@ class StatusLED(QAbstractButton):
47
46
  self._postitve = QtBlack
48
47
  self._negative = QtBlack
49
48
  self._color = QtBlack
50
- self._state = nwTrinary.NEUTRAL
49
+ self._state = None
51
50
  self._bPx = CONFIG.pxInt(1)
52
51
  self.setFixedWidth(sW)
53
52
  self.setFixedHeight(sH)
54
53
  return
55
54
 
56
55
  @property
57
- def state(self) -> nwTrinary:
56
+ def state(self) -> bool | None:
58
57
  """The current state of the LED."""
59
58
  return self._state
60
59
 
@@ -66,11 +65,11 @@ class StatusLED(QAbstractButton):
66
65
  self.setState(self._state)
67
66
  return
68
67
 
69
- def setState(self, state: nwTrinary) -> None:
68
+ def setState(self, state: bool | None) -> None:
70
69
  """Set the colour state."""
71
- if state == nwTrinary.POSITIVE:
70
+ if state is True:
72
71
  self._color = self._postitve
73
- elif state == nwTrinary.NEGATIVE:
72
+ elif state is False:
74
73
  self._color = self._negative
75
74
  else:
76
75
  self._color = self._neutral
@@ -104,17 +104,18 @@ class BlockTyp(IntEnum):
104
104
 
105
105
  EMPTY = 1 # Empty line (new paragraph)
106
106
  TITLE = 2 # Title
107
- HEAD1 = 3 # Heading 1
108
- HEAD2 = 4 # Heading 2
109
- HEAD3 = 5 # Heading 3
110
- HEAD4 = 6 # Heading 4
111
- TEXT = 7 # Text line
112
- SEP = 8 # Scene separator
113
- SKIP = 9 # Paragraph break
114
- SUMMARY = 10 # Synopsis/short comment
115
- NOTE = 11 # Note
116
- COMMENT = 12 # Comment
117
- KEYWORD = 13 # Tag/reference keywords
107
+ PART = 3 # Partition
108
+ HEAD1 = 4 # Heading 1 or Chapter
109
+ HEAD2 = 5 # Heading 2 or Scene
110
+ HEAD3 = 6 # Heading 3 or Section
111
+ HEAD4 = 7 # Heading 4
112
+ TEXT = 8 # Text line
113
+ SEP = 9 # Scene separator
114
+ SKIP = 10 # Paragraph break
115
+ SUMMARY = 11 # Synopsis/short comment
116
+ NOTE = 12 # Note
117
+ COMMENT = 13 # Comment
118
+ KEYWORD = 14 # Tag/reference keywords
118
119
 
119
120
 
120
121
  class BlockFmt(Flag):
@@ -271,7 +271,7 @@ class ToDocX(Tokenizer):
271
271
  if tType == BlockTyp.TEXT:
272
272
  self._processFragments(par, S_NORM, tText, tFormat)
273
273
 
274
- elif tType == BlockTyp.TITLE:
274
+ elif tType in (BlockTyp.TITLE, BlockTyp.PART):
275
275
  self._processFragments(par, S_TITLE, tText, tFormat)
276
276
 
277
277
  elif tType == BlockTyp.HEAD1:
@@ -30,7 +30,7 @@ from pathlib import Path
30
30
  from time import time
31
31
 
32
32
  from novelwriter.common import formatTimeStamp
33
- from novelwriter.constants import nwHtmlUnicode
33
+ from novelwriter.constants import nwHtmlUnicode, nwStyles
34
34
  from novelwriter.core.project import NWProject
35
35
  from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape
36
36
  from novelwriter.formats.tokenizer import Tokenizer
@@ -130,20 +130,6 @@ class ToHtml(Tokenizer):
130
130
 
131
131
  def doConvert(self) -> None:
132
132
  """Convert the list of text tokens into an HTML document."""
133
- if self._isNovel:
134
- # For story files, we bump the titles one level up
135
- h1Cl = " class='title'"
136
- h1 = "h1"
137
- h2 = "h1"
138
- h3 = "h2"
139
- h4 = "h3"
140
- else:
141
- h1Cl = ""
142
- h1 = "h1"
143
- h2 = "h2"
144
- h3 = "h3"
145
- h4 = "h4"
146
-
147
133
  lines = []
148
134
  for tType, tMeta, tText, tFmt, tStyle in self._blocks:
149
135
 
@@ -213,25 +199,25 @@ class ToHtml(Tokenizer):
213
199
  if tType == BlockTyp.TEXT:
214
200
  lines.append(f"<p{hStyle}>{self._formatText(tText, tFmt)}</p>\n")
215
201
 
216
- elif tType == BlockTyp.TITLE:
202
+ elif tType in (BlockTyp.TITLE, BlockTyp.PART):
217
203
  tHead = tText.replace("\n", "<br>")
218
204
  lines.append(f"<h1 class='title'{hStyle}>{aNm}{tHead}</h1>\n")
219
205
 
220
206
  elif tType == BlockTyp.HEAD1:
221
207
  tHead = tText.replace("\n", "<br>")
222
- lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}</{h1}>\n")
208
+ lines.append(f"<h1{hStyle}>{aNm}{tHead}</h1>\n")
223
209
 
224
210
  elif tType == BlockTyp.HEAD2:
225
211
  tHead = tText.replace("\n", "<br>")
226
- lines.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
212
+ lines.append(f"<h2{hStyle}>{aNm}{tHead}</h2>\n")
227
213
 
228
214
  elif tType == BlockTyp.HEAD3:
229
215
  tHead = tText.replace("\n", "<br>")
230
- lines.append(f"<{h3}{hStyle}>{aNm}{tHead}</{h3}>\n")
216
+ lines.append(f"<h3{hStyle}>{aNm}{tHead}</h3>\n")
231
217
 
232
218
  elif tType == BlockTyp.HEAD4:
233
219
  tHead = tText.replace("\n", "<br>")
234
- lines.append(f"<{h4}{hStyle}>{aNm}{tHead}</{h4}>\n")
220
+ lines.append(f"<h4{hStyle}>{aNm}{tHead}</h4>\n")
235
221
 
236
222
  elif tType == BlockTyp.SEP:
237
223
  lines.append(f"<p class='sep'{hStyle}>{tText}</p>\n")
@@ -310,9 +296,7 @@ class ToHtml(Tokenizer):
310
296
  "</style>\n"
311
297
  "</head>\n"
312
298
  "<body>\n"
313
- "<article>\n"
314
299
  "{body:s}\n"
315
- "</article>\n"
316
300
  "</body>\n"
317
301
  "</html>\n"
318
302
  ).format(
@@ -359,6 +343,12 @@ class ToHtml(Tokenizer):
359
343
  mtSP = self._marginSep[0]
360
344
  mbSP = self._marginSep[1]
361
345
 
346
+ fSz0 = nwStyles.H_SIZES[0]
347
+ fSz1 = nwStyles.H_SIZES[1]
348
+ fSz2 = nwStyles.H_SIZES[2]
349
+ fSz3 = nwStyles.H_SIZES[3]
350
+ fSz4 = nwStyles.H_SIZES[4]
351
+
362
352
  font = self._textFont
363
353
  fFam = font.family()
364
354
  fSz = font.pointSize()
@@ -379,12 +369,25 @@ class ToHtml(Tokenizer):
379
369
  styles.append(f"a {{color: {lColor};}}")
380
370
  styles.append(f"mark {{background: {mColor};}}")
381
371
  styles.append(f"h1, h2, h3, h4 {{color: {hColor}; page-break-after: avoid;}}")
382
- styles.append(f"h1 {{margin-top: {mtH1:.2f}em; margin-bottom: {mbH1:.2f}em;}}")
383
- styles.append(f"h2 {{margin-top: {mtH2:.2f}em; margin-bottom: {mbH2:.2f}em;}}")
384
- styles.append(f"h3 {{margin-top: {mtH3:.2f}em; margin-bottom: {mbH3:.2f}em;}}")
385
- styles.append(f"h4 {{margin-top: {mtH4:.2f}em; margin-bottom: {mbH4:.2f}em;}}")
386
372
  styles.append(
387
- f".title {{font-size: 2.5em; margin-top: {mtH0:.2f}em; margin-bottom: {mbH0:.2f}em;}}"
373
+ f"h1 {{font-size: {fSz1:.2f}em; "
374
+ f"margin-top: {mtH1:.2f}em; margin-bottom: {mbH1:.2f}em;}}"
375
+ )
376
+ styles.append(
377
+ f"h2 {{font-size: {fSz2:.2f}em; "
378
+ f"margin-top: {mtH2:.2f}em; margin-bottom: {mbH2:.2f}em;}}"
379
+ )
380
+ styles.append(
381
+ f"h3 {{font-size: {fSz3:.2f}em; "
382
+ f"margin-top: {mtH3:.2f}em; margin-bottom: {mbH3:.2f}em;}}"
383
+ )
384
+ styles.append(
385
+ f"h4 {{font-size: {fSz4:.2f}em; "
386
+ f"margin-top: {mtH4:.2f}em; margin-bottom: {mbH4:.2f}em;}}"
387
+ )
388
+ styles.append(
389
+ f".title {{font-size: {fSz0:.2f}em; "
390
+ f"margin-top: {mtH0:.2f}em; margin-bottom: {mbH0:.2f}em;}}"
388
391
  )
389
392
  styles.append(
390
393
  f".sep {{text-align: center; margin-top: {mtSP:.2f}em; margin-bottom: {mbSP:.2f}em;}}"