novelWriter 2.5.3__py3-none-any.whl → 2.6b2__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 (83) hide show
  1. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/METADATA +1 -1
  2. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/RECORD +80 -60
  3. novelwriter/__init__.py +49 -10
  4. novelwriter/assets/i18n/project_en_GB.json +1 -0
  5. novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
  6. novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
  7. novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
  8. novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
  9. novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
  10. novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
  11. novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
  12. novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
  13. novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
  14. novelwriter/assets/icons/typicons_light/icons.conf +8 -0
  15. novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
  16. novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
  17. novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
  18. novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
  19. novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
  20. novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
  21. novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
  22. novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
  23. novelwriter/assets/manual.pdf +0 -0
  24. novelwriter/assets/sample.zip +0 -0
  25. novelwriter/common.py +100 -2
  26. novelwriter/config.py +25 -15
  27. novelwriter/constants.py +168 -60
  28. novelwriter/core/buildsettings.py +66 -39
  29. novelwriter/core/coretools.py +145 -147
  30. novelwriter/core/docbuild.py +132 -170
  31. novelwriter/core/index.py +38 -37
  32. novelwriter/core/item.py +41 -8
  33. novelwriter/core/itemmodel.py +518 -0
  34. novelwriter/core/options.py +4 -1
  35. novelwriter/core/project.py +67 -89
  36. novelwriter/core/spellcheck.py +9 -14
  37. novelwriter/core/status.py +7 -5
  38. novelwriter/core/tree.py +268 -287
  39. novelwriter/dialogs/docmerge.py +7 -17
  40. novelwriter/dialogs/preferences.py +46 -33
  41. novelwriter/dialogs/projectsettings.py +5 -5
  42. novelwriter/enum.py +36 -23
  43. novelwriter/extensions/configlayout.py +27 -12
  44. novelwriter/extensions/modified.py +13 -1
  45. novelwriter/extensions/pagedsidebar.py +5 -5
  46. novelwriter/formats/shared.py +155 -0
  47. novelwriter/formats/todocx.py +1191 -0
  48. novelwriter/formats/tohtml.py +451 -0
  49. novelwriter/{core → formats}/tokenizer.py +487 -491
  50. novelwriter/formats/tomarkdown.py +217 -0
  51. novelwriter/{core → formats}/toodt.py +311 -432
  52. novelwriter/formats/toqdoc.py +484 -0
  53. novelwriter/formats/toraw.py +91 -0
  54. novelwriter/gui/doceditor.py +342 -284
  55. novelwriter/gui/dochighlight.py +96 -84
  56. novelwriter/gui/docviewer.py +88 -31
  57. novelwriter/gui/docviewerpanel.py +17 -25
  58. novelwriter/gui/editordocument.py +17 -2
  59. novelwriter/gui/itemdetails.py +25 -28
  60. novelwriter/gui/mainmenu.py +129 -63
  61. novelwriter/gui/noveltree.py +45 -47
  62. novelwriter/gui/outline.py +196 -249
  63. novelwriter/gui/projtree.py +594 -1241
  64. novelwriter/gui/search.py +9 -10
  65. novelwriter/gui/sidebar.py +7 -6
  66. novelwriter/gui/theme.py +10 -5
  67. novelwriter/guimain.py +100 -196
  68. novelwriter/shared.py +66 -27
  69. novelwriter/text/counting.py +2 -0
  70. novelwriter/text/patterns.py +168 -60
  71. novelwriter/tools/manusbuild.py +14 -12
  72. novelwriter/tools/manuscript.py +120 -78
  73. novelwriter/tools/manussettings.py +424 -291
  74. novelwriter/tools/welcome.py +4 -4
  75. novelwriter/tools/writingstats.py +3 -3
  76. novelwriter/types.py +23 -7
  77. novelwriter/core/tohtml.py +0 -530
  78. novelwriter/core/tomarkdown.py +0 -252
  79. novelwriter/core/toqdoc.py +0 -419
  80. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/LICENSE.md +0 -0
  81. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/WHEEL +0 -0
  82. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/entry_points.txt +0 -0
  83. {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/top_level.txt +0 -0
novelwriter/common.py CHANGED
@@ -29,6 +29,7 @@ import unicodedata
29
29
  import uuid
30
30
  import xml.etree.ElementTree as ET
31
31
 
32
+ from collections.abc import Callable
32
33
  from configparser import ConfigParser
33
34
  from datetime import datetime
34
35
  from pathlib import Path
@@ -36,8 +37,8 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar
36
37
  from urllib.parse import urljoin
37
38
  from urllib.request import pathname2url
38
39
 
39
- from PyQt5.QtCore import QCoreApplication, QUrl
40
- from PyQt5.QtGui import QColor, QDesktopServices, QFont, QFontInfo
40
+ from PyQt5.QtCore import QCoreApplication, QMimeData, QUrl
41
+ from PyQt5.QtGui import QColor, QDesktopServices, QFont, QFontDatabase, QFontInfo
41
42
 
42
43
  from novelwriter.constants import nwConst, nwLabels, nwUnicode, trConst
43
44
  from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
@@ -204,6 +205,14 @@ def checkIntTuple(value: int, valid: tuple | list | set, default: int) -> int:
204
205
  return default
205
206
 
206
207
 
208
+ def firstFloat(*args: Any) -> float:
209
+ """Return the first value that is a float."""
210
+ for arg in args:
211
+ if isinstance(arg, float):
212
+ return arg
213
+ return 0.0
214
+
215
+
207
216
  ##
208
217
  # Formatting Functions
209
218
  ##
@@ -406,6 +415,10 @@ def numberToRoman(value: int, toLower: bool = False) -> str:
406
415
  return roman.lower() if toLower else roman
407
416
 
408
417
 
418
+ ##
419
+ # Qt Helpers
420
+ ##
421
+
409
422
  def cssCol(col: QColor, alpha: int | None = None) -> str:
410
423
  """Convert a QColor object to an rgba entry to use in CSS."""
411
424
  return f"rgba({col.red()}, {col.green()}, {col.blue()}, {alpha or col.alpha()})"
@@ -421,6 +434,48 @@ def describeFont(font: QFont) -> str:
421
434
  return "Error"
422
435
 
423
436
 
437
+ def fontMatcher(font: QFont) -> QFont:
438
+ """Make sure the font is the correct family, if possible. This
439
+ ensures that Qt doesn't re-use another font under the hood. The
440
+ default Qt5 font matching algorithm doesn't handle well changing
441
+ application fonts at runtime.
442
+ """
443
+ info = QFontInfo(font)
444
+ if (famRequest := font.family()) != (famActual := info.family()):
445
+ logger.warning("Font mismatch: Requested '%s', but got '%s'", famRequest, famActual)
446
+ db = QFontDatabase()
447
+ if famRequest in db.families():
448
+ styleRequest, sizeRequest = font.styleName(), font.pointSize()
449
+ logger.info("Lookup: %s, %s, %d pt", famRequest, styleRequest, sizeRequest)
450
+ temp = db.font(famRequest, styleRequest, sizeRequest)
451
+ temp.setPointSize(sizeRequest) # Make sure it isn't changed
452
+ famFound, styleFound, sizeFound = temp.family(), temp.styleName(), temp.pointSize()
453
+ if famFound == famRequest:
454
+ logger.info("Found: %s, %s, %d pt", famFound, styleFound, sizeFound)
455
+ return temp
456
+ logger.warning("Could not find a font match in the font database")
457
+ logger.warning("If you just changed font, you may need to restart the application")
458
+ return font
459
+
460
+
461
+ def qtLambda(func: Callable, *args: Any, **kwargs: Any) -> Callable:
462
+ """A replacement for Python lambdas that works for Qt slots."""
463
+ def wrapper(*a_: Any) -> None:
464
+ func(*args, **kwargs)
465
+ return wrapper
466
+
467
+
468
+ def encodeMimeHandles(mimeData: QMimeData, handles: list[str]) -> None:
469
+ """Encode handles into a mime data object."""
470
+ mimeData.setData(nwConst.MIME_HANDLE, b"|".join(h.encode() for h in handles))
471
+ return
472
+
473
+
474
+ def decodeMimeHandles(mimeData: QMimeData) -> list[str]:
475
+ """Decode and split a mime data object with handles."""
476
+ return mimeData.data(nwConst.MIME_HANDLE).data().decode().split("|")
477
+
478
+
424
479
  ##
425
480
  # Encoder Functions
426
481
  ##
@@ -472,6 +527,10 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
472
527
  return "".join(buffer)
473
528
 
474
529
 
530
+ ##
531
+ # XML Helpers
532
+ ##
533
+
475
534
  def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
476
535
  """A modified version of the XML indent function in the standard
477
536
  library. It behaves more closely to how the one from lxml does.
@@ -515,6 +574,45 @@ def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
515
574
  return
516
575
 
517
576
 
577
+ def xmlElement(
578
+ tag: str,
579
+ text: str | int | float | bool | None = None,
580
+ *,
581
+ attrib: dict | None = None,
582
+ tail: str | None = None,
583
+ ) -> ET.Element:
584
+ """A custom implementation of Element with more arguments."""
585
+ xSub = ET.Element(tag, attrib=attrib or {})
586
+ if text is not None:
587
+ if isinstance(text, bool):
588
+ xSub.text = str(text).lower()
589
+ else:
590
+ xSub.text = str(text)
591
+ if tail is not None:
592
+ xSub.tail = tail
593
+ return xSub
594
+
595
+
596
+ def xmlSubElem(
597
+ parent: ET.Element,
598
+ tag: str,
599
+ text: str | int | float | bool | None = None,
600
+ *,
601
+ attrib: dict | None = None,
602
+ tail: str | None = None,
603
+ ) -> ET.Element:
604
+ """A custom implementation of SubElement with more arguments."""
605
+ xSub = ET.SubElement(parent, tag, attrib=attrib or {})
606
+ if text is not None:
607
+ if isinstance(text, bool):
608
+ xSub.text = str(text).lower()
609
+ else:
610
+ xSub.text = str(text)
611
+ if tail is not None:
612
+ xSub.tail = tail
613
+ return xSub
614
+
615
+
518
616
  ##
519
617
  # File and File System Functions
520
618
  ##
novelwriter/config.py CHANGED
@@ -40,7 +40,10 @@ from PyQt5.QtCore import (
40
40
  from PyQt5.QtGui import QFont, QFontDatabase
41
41
  from PyQt5.QtWidgets import QApplication
42
42
 
43
- from novelwriter.common import NWConfigParser, checkInt, checkPath, describeFont, formatTimeStamp
43
+ from novelwriter.common import (
44
+ NWConfigParser, checkInt, checkPath, describeFont, fontMatcher,
45
+ formatTimeStamp
46
+ )
44
47
  from novelwriter.constants import nwFiles, nwUnicode
45
48
  from novelwriter.error import formatException, logException
46
49
 
@@ -161,8 +164,9 @@ class Config:
161
164
 
162
165
  self.dialogStyle = 2 # Quote type to use for dialogue
163
166
  self.allowOpenDial = True # Allow open-ended dialogue quotes
164
- self.narratorBreak = "" # Symbol to use for narrator break
165
167
  self.dialogLine = "" # Symbol to use for dialogue line
168
+ self.narratorBreak = "" # Symbol to use for narrator break
169
+ self.narratorDialog = "" # Symbol for alternating between dialogue and narrator
166
170
  self.altDialogOpen = "" # Alternative dialog symbol, open
167
171
  self.altDialogClose = "" # Alternative dialog symbol, close
168
172
  self.highlightEmph = True # Add colour to text emphasis
@@ -252,6 +256,10 @@ class Config:
252
256
  def hasError(self) -> bool:
253
257
  return self._hasError
254
258
 
259
+ @property
260
+ def locale(self) -> QLocale:
261
+ return self._dLocale
262
+
255
263
  @property
256
264
  def recentProjects(self) -> RecentProjects:
257
265
  return self._recentProjects
@@ -364,10 +372,11 @@ class Config:
364
372
  def setGuiFont(self, value: QFont | str | None) -> None:
365
373
  """Update the GUI's font style from settings."""
366
374
  if isinstance(value, QFont):
367
- self.guiFont = value
375
+ self.guiFont = fontMatcher(value)
368
376
  elif value and isinstance(value, str):
369
- self.guiFont = QFont()
370
- self.guiFont.fromString(value)
377
+ font = QFont()
378
+ font.fromString(value)
379
+ self.guiFont = fontMatcher(font)
371
380
  else:
372
381
  font = QFont()
373
382
  fontDB = QFontDatabase()
@@ -377,11 +386,9 @@ class Config:
377
386
  font.setPointSize(10)
378
387
  else:
379
388
  font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont)
380
- self.guiFont = font
389
+ self.guiFont = fontMatcher(font)
381
390
  logger.debug("GUI font set to: %s", describeFont(font))
382
-
383
391
  QApplication.setFont(self.guiFont)
384
-
385
392
  return
386
393
 
387
394
  def setTextFont(self, value: QFont | str | None) -> None:
@@ -389,10 +396,11 @@ class Config:
389
396
  set to default font.
390
397
  """
391
398
  if isinstance(value, QFont):
392
- self.textFont = value
399
+ self.textFont = fontMatcher(value)
393
400
  elif value and isinstance(value, str):
394
- self.textFont = QFont()
395
- self.textFont.fromString(value)
401
+ font = QFont()
402
+ font.fromString(value)
403
+ self.textFont = fontMatcher(font)
396
404
  else:
397
405
  fontDB = QFontDatabase()
398
406
  fontFam = fontDB.families()
@@ -406,8 +414,8 @@ class Config:
406
414
  font.setPointSize(12)
407
415
  else:
408
416
  font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont)
409
- self.textFont = font
410
- logger.debug("Text font set to: %s", describeFont(font))
417
+ self.textFont = fontMatcher(font)
418
+ logger.debug("Text font set to: %s", describeFont(self.textFont))
411
419
  return
412
420
 
413
421
  ##
@@ -653,10 +661,11 @@ class Config:
653
661
  self.showFullPath = conf.rdBool(sec, "showfullpath", self.showFullPath)
654
662
  self.dialogStyle = conf.rdInt(sec, "dialogstyle", self.dialogStyle)
655
663
  self.allowOpenDial = conf.rdBool(sec, "allowopendial", self.allowOpenDial)
664
+ self.dialogLine = conf.rdStr(sec, "dialogline", self.dialogLine)
656
665
  self.narratorBreak = conf.rdStr(sec, "narratorbreak", self.narratorBreak)
666
+ self.narratorDialog = conf.rdStr(sec, "narratordialog", self.narratorDialog)
657
667
  self.altDialogOpen = conf.rdStr(sec, "altdialogopen", self.altDialogOpen)
658
668
  self.altDialogClose = conf.rdStr(sec, "altdialogclose", self.altDialogClose)
659
- self.dialogLine = conf.rdStr(sec, "dialogline", self.dialogLine)
660
669
  self.highlightEmph = conf.rdBool(sec, "highlightemph", self.highlightEmph)
661
670
  self.stopWhenIdle = conf.rdBool(sec, "stopwhenidle", self.stopWhenIdle)
662
671
  self.userIdleTime = conf.rdInt(sec, "useridletime", self.userIdleTime)
@@ -762,10 +771,11 @@ class Config:
762
771
  "showfullpath": str(self.showFullPath),
763
772
  "dialogstyle": str(self.dialogStyle),
764
773
  "allowopendial": str(self.allowOpenDial),
774
+ "dialogline": str(self.dialogLine),
765
775
  "narratorbreak": str(self.narratorBreak),
776
+ "narratordialog": str(self.narratorDialog),
766
777
  "altdialogopen": str(self.altDialogOpen),
767
778
  "altdialogclose": str(self.altDialogClose),
768
- "dialogline": str(self.dialogLine),
769
779
  "highlightemph": str(self.highlightEmph),
770
780
  "stopwhenidle": str(self.stopWhenIdle),
771
781
  "useridletime": str(self.userIdleTime),
novelwriter/constants.py CHANGED
@@ -53,6 +53,9 @@ class nwConst:
53
53
  # Requests
54
54
  USER_AGENT = "Mozilla/5.0 (compatible; novelWriter (Python))"
55
55
 
56
+ # Mime Types
57
+ MIME_HANDLE = "text/vnd.novelwriter.handle"
58
+
56
59
  # Gui Settings
57
60
  STATUS_MSG_TIMEOUT = 15000 # milliseconds
58
61
  MAX_SEARCH_RESULT = 1000
@@ -60,11 +63,14 @@ class nwConst:
60
63
 
61
64
  class nwRegEx:
62
65
 
66
+ URL = r"https?://(?:www\.|(?!www))[\w/()@:%_\+-.~#?&=]+"
67
+ WORDS = r"\b[^\s\-\+\/–—\[\]:]+\b"
68
+ BREAK = r"(?i)(?<!\\)(\[br\]\n?)"
63
69
  FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
64
70
  FMT_EB = r"(?<![\w\\])(\*{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
65
71
  FMT_ST = r"(?<![\w\\])(~{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
66
- FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:b|i|s|u|m|sup|sub)\])"
67
- FMT_SV = r"(?i)(?<!\\)(\[(?:footnote):)(.+?)(?<!\\)(\])"
72
+ FMT_SC = r"(?i)(?<!\\)(\[(?:b|/b|i|/i|s|/s|u|/u|m|/m|sup|/sup|sub|/sub|br)\])"
73
+ FMT_SV = r"(?i)(?<!\\)(\[(?:footnote|field):)(.+?)(?<!\\)(\])"
68
74
 
69
75
 
70
76
  class nwShortcode:
@@ -83,21 +89,49 @@ class nwShortcode:
83
89
  SUP_C = "[/sup]"
84
90
  SUB_O = "[sub]"
85
91
  SUB_C = "[/sub]"
92
+ BREAK = "[br]"
86
93
 
87
94
  FOOTNOTE_B = "[footnote:"
95
+ FIELD_B = "[field:"
88
96
 
89
97
  COMMENT_STYLES = {
90
98
  nwComment.FOOTNOTE: "[footnote:{0}]",
91
99
  nwComment.COMMENT: "[comment:{0}]",
92
100
  }
93
101
 
102
+ FIELD_VALUE = "[field:{0}]"
103
+
94
104
 
95
- class nwHeaders:
105
+ class nwStyles:
96
106
 
97
107
  H_VALID = ("H0", "H1", "H2", "H3", "H4")
98
108
  H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
99
109
  H_SIZES = {0: 2.50, 1: 2.00, 2: 1.75, 3: 1.50, 4: 1.25}
100
110
 
111
+ T_NORMAL = 1.0
112
+ T_SMALL = 0.8
113
+
114
+ T_LABEL = {
115
+ "H0": QT_TRANSLATE_NOOP("Constant", "Title"),
116
+ "H1": QT_TRANSLATE_NOOP("Constant", "Heading 1 (Partition)"),
117
+ "H2": QT_TRANSLATE_NOOP("Constant", "Heading 2 (Chapter)"),
118
+ "H3": QT_TRANSLATE_NOOP("Constant", "Heading 3 (Scene)"),
119
+ "H4": QT_TRANSLATE_NOOP("Constant", "Heading 4 (Section)"),
120
+ "TT": QT_TRANSLATE_NOOP("Constant", "Text Paragraph"),
121
+ "SP": QT_TRANSLATE_NOOP("Constant", "Scene Separator"),
122
+ }
123
+ T_MARGIN = {
124
+ "H0": (1.50, 0.60), # Title margins (top, bottom)
125
+ "H1": (1.50, 0.60), # Heading 1 margins (top, bottom)
126
+ "H2": (1.50, 0.60), # Heading 2 margins (top, bottom)
127
+ "H3": (1.20, 0.60), # Heading 3 margins (top, bottom)
128
+ "H4": (1.20, 0.60), # Heading 4 margins (top, bottom)
129
+ "TT": (0.00, 0.60), # Text margins (top, bottom)
130
+ "SP": (1.20, 1.20), # Separator margins (top, bottom)
131
+ "MT": (0.00, 0.60), # Meta margins (top, bottom)
132
+ "FT": (1.40, 0.40), # Footnote margins (left, bottom)
133
+ }
134
+
101
135
 
102
136
  class nwFiles:
103
137
 
@@ -121,34 +155,40 @@ class nwFiles:
121
155
 
122
156
  class nwKeyWords:
123
157
 
124
- TAG_KEY = "@tag"
125
- POV_KEY = "@pov"
126
- FOCUS_KEY = "@focus"
127
- CHAR_KEY = "@char"
128
- PLOT_KEY = "@plot"
129
- TIME_KEY = "@time"
130
- WORLD_KEY = "@location"
131
- OBJECT_KEY = "@object"
132
- ENTITY_KEY = "@entity"
133
- CUSTOM_KEY = "@custom"
158
+ TAG_KEY = "@tag"
159
+ POV_KEY = "@pov"
160
+ FOCUS_KEY = "@focus"
161
+ CHAR_KEY = "@char"
162
+ PLOT_KEY = "@plot"
163
+ TIME_KEY = "@time"
164
+ WORLD_KEY = "@location"
165
+ OBJECT_KEY = "@object"
166
+ ENTITY_KEY = "@entity"
167
+ CUSTOM_KEY = "@custom"
168
+ STORY_KEY = "@story"
169
+ MENTION_KEY = "@mention"
170
+
171
+ # Note: The order here affects the order of menu entries
172
+ ALL_KEYS = [
173
+ TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
174
+ OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY, STORY_KEY, MENTION_KEY,
175
+ ]
134
176
 
135
177
  # Set of Valid Keys
136
- VALID_KEYS = {
137
- TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY,
138
- WORLD_KEY, OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY
139
- }
178
+ VALID_KEYS = set(ALL_KEYS)
140
179
 
141
180
  # Map from Keys to Item Class
142
181
  KEY_CLASS = {
143
- POV_KEY: nwItemClass.CHARACTER,
144
- FOCUS_KEY: nwItemClass.CHARACTER,
145
- CHAR_KEY: nwItemClass.CHARACTER,
146
- PLOT_KEY: nwItemClass.PLOT,
147
- TIME_KEY: nwItemClass.TIMELINE,
148
- WORLD_KEY: nwItemClass.WORLD,
149
- OBJECT_KEY: nwItemClass.OBJECT,
150
- ENTITY_KEY: nwItemClass.ENTITY,
151
- CUSTOM_KEY: nwItemClass.CUSTOM,
182
+ POV_KEY: nwItemClass.CHARACTER,
183
+ FOCUS_KEY: nwItemClass.CHARACTER,
184
+ CHAR_KEY: nwItemClass.CHARACTER,
185
+ PLOT_KEY: nwItemClass.PLOT,
186
+ TIME_KEY: nwItemClass.TIMELINE,
187
+ WORLD_KEY: nwItemClass.WORLD,
188
+ OBJECT_KEY: nwItemClass.OBJECT,
189
+ ENTITY_KEY: nwItemClass.ENTITY,
190
+ CUSTOM_KEY: nwItemClass.CUSTOM,
191
+ STORY_KEY: nwItemClass.NOVEL,
152
192
  }
153
193
 
154
194
 
@@ -165,6 +205,29 @@ class nwLists:
165
205
  ]
166
206
 
167
207
 
208
+ class nwStats:
209
+
210
+ CHARS_ALL = "allChars"
211
+ CHARS_TEXT = "textChars"
212
+ CHARS_TITLE = "titleChars"
213
+ PARAGRAPHS = "paragraphCount"
214
+ TITLES = "titleCount"
215
+ WCHARS_ALL = "allWordChars"
216
+ WCHARS_TEXT = "textWordChars"
217
+ WCHARS_TITLE = "titleWordChars"
218
+ WORDS_ALL = "allWords"
219
+ WORDS_TEXT = "textWords"
220
+ WORDS_TITLE = "titleWords"
221
+
222
+ # Note: The order here affects the order of menu entries
223
+ ALL_FIELDS = [
224
+ WORDS_ALL, WORDS_TEXT, WORDS_TITLE,
225
+ CHARS_ALL, CHARS_TEXT, CHARS_TITLE,
226
+ WCHARS_ALL, WCHARS_TEXT, WCHARS_TITLE,
227
+ PARAGRAPHS, TITLES,
228
+ ]
229
+
230
+
168
231
  class nwLabels:
169
232
 
170
233
  CLASS_NAME = {
@@ -211,54 +274,94 @@ class nwLabels:
211
274
  "doc_h4": QT_TRANSLATE_NOOP("Constant", "Novel Section"),
212
275
  "note": QT_TRANSLATE_NOOP("Constant", "Project Note"),
213
276
  }
277
+ ACTIVE_NAME = {
278
+ "checked": QT_TRANSLATE_NOOP("Constant", "Active"),
279
+ "unchecked": QT_TRANSLATE_NOOP("Constant", "Inactive"),
280
+ }
214
281
  KEY_NAME = {
215
- nwKeyWords.TAG_KEY: QT_TRANSLATE_NOOP("Constant", "Tag"),
216
- nwKeyWords.POV_KEY: QT_TRANSLATE_NOOP("Constant", "Point of View"),
217
- nwKeyWords.FOCUS_KEY: QT_TRANSLATE_NOOP("Constant", "Focus"),
218
- nwKeyWords.CHAR_KEY: QT_TRANSLATE_NOOP("Constant", "Characters"),
219
- nwKeyWords.PLOT_KEY: QT_TRANSLATE_NOOP("Constant", "Plot"),
220
- nwKeyWords.TIME_KEY: QT_TRANSLATE_NOOP("Constant", "Timeline"),
221
- nwKeyWords.WORLD_KEY: QT_TRANSLATE_NOOP("Constant", "Locations"),
222
- nwKeyWords.OBJECT_KEY: QT_TRANSLATE_NOOP("Constant", "Objects"),
223
- nwKeyWords.ENTITY_KEY: QT_TRANSLATE_NOOP("Constant", "Entities"),
224
- nwKeyWords.CUSTOM_KEY: QT_TRANSLATE_NOOP("Constant", "Custom"),
282
+ nwKeyWords.TAG_KEY: QT_TRANSLATE_NOOP("Constant", "Tag"),
283
+ nwKeyWords.POV_KEY: QT_TRANSLATE_NOOP("Constant", "Point of View"),
284
+ nwKeyWords.FOCUS_KEY: QT_TRANSLATE_NOOP("Constant", "Focus"),
285
+ nwKeyWords.CHAR_KEY: QT_TRANSLATE_NOOP("Constant", "Characters"),
286
+ nwKeyWords.PLOT_KEY: QT_TRANSLATE_NOOP("Constant", "Plot"),
287
+ nwKeyWords.TIME_KEY: QT_TRANSLATE_NOOP("Constant", "Timeline"),
288
+ nwKeyWords.WORLD_KEY: QT_TRANSLATE_NOOP("Constant", "Locations"),
289
+ nwKeyWords.OBJECT_KEY: QT_TRANSLATE_NOOP("Constant", "Objects"),
290
+ nwKeyWords.ENTITY_KEY: QT_TRANSLATE_NOOP("Constant", "Entities"),
291
+ nwKeyWords.CUSTOM_KEY: QT_TRANSLATE_NOOP("Constant", "Custom"),
292
+ nwKeyWords.STORY_KEY: QT_TRANSLATE_NOOP("Constant", "Story"),
293
+ nwKeyWords.MENTION_KEY: QT_TRANSLATE_NOOP("Constant", "Mentions"),
294
+ }
295
+ KEY_SHORTCUT = {
296
+ nwKeyWords.TAG_KEY: "Ctrl+K, G",
297
+ nwKeyWords.POV_KEY: "Ctrl+K, V",
298
+ nwKeyWords.FOCUS_KEY: "Ctrl+K, F",
299
+ nwKeyWords.CHAR_KEY: "Ctrl+K, C",
300
+ nwKeyWords.PLOT_KEY: "Ctrl+K, P",
301
+ nwKeyWords.TIME_KEY: "Ctrl+K, T",
302
+ nwKeyWords.WORLD_KEY: "Ctrl+K, L",
303
+ nwKeyWords.OBJECT_KEY: "Ctrl+K, O",
304
+ nwKeyWords.ENTITY_KEY: "Ctrl+K, E",
305
+ nwKeyWords.CUSTOM_KEY: "Ctrl+K, X",
306
+ nwKeyWords.STORY_KEY: "Ctrl+K, N",
307
+ nwKeyWords.MENTION_KEY: "Ctrl+K, M",
225
308
  }
226
309
  OUTLINE_COLS = {
227
- nwOutline.TITLE: QT_TRANSLATE_NOOP("Constant", "Title"),
228
- nwOutline.LEVEL: QT_TRANSLATE_NOOP("Constant", "Level"),
229
- nwOutline.LABEL: QT_TRANSLATE_NOOP("Constant", "Document"),
230
- nwOutline.LINE: QT_TRANSLATE_NOOP("Constant", "Line"),
231
- nwOutline.CCOUNT: QT_TRANSLATE_NOOP("Constant", "Chars"),
232
- nwOutline.WCOUNT: QT_TRANSLATE_NOOP("Constant", "Words"),
233
- nwOutline.PCOUNT: QT_TRANSLATE_NOOP("Constant", "Pars"),
234
- nwOutline.POV: QT_TRANSLATE_NOOP("Constant", "POV"),
235
- nwOutline.FOCUS: QT_TRANSLATE_NOOP("Constant", "Focus"),
236
- nwOutline.CHAR: KEY_NAME[nwKeyWords.CHAR_KEY],
237
- nwOutline.PLOT: KEY_NAME[nwKeyWords.PLOT_KEY],
238
- nwOutline.WORLD: KEY_NAME[nwKeyWords.WORLD_KEY],
239
- nwOutline.TIME: KEY_NAME[nwKeyWords.TIME_KEY],
240
- nwOutline.OBJECT: KEY_NAME[nwKeyWords.OBJECT_KEY],
241
- nwOutline.ENTITY: KEY_NAME[nwKeyWords.ENTITY_KEY],
242
- nwOutline.CUSTOM: KEY_NAME[nwKeyWords.CUSTOM_KEY],
243
- nwOutline.SYNOP: QT_TRANSLATE_NOOP("Constant", "Synopsis"),
310
+ nwOutline.TITLE: QT_TRANSLATE_NOOP("Constant", "Title"),
311
+ nwOutline.LEVEL: QT_TRANSLATE_NOOP("Constant", "Level"),
312
+ nwOutline.LABEL: QT_TRANSLATE_NOOP("Constant", "Document"),
313
+ nwOutline.LINE: QT_TRANSLATE_NOOP("Constant", "Line"),
314
+ nwOutline.STATUS: QT_TRANSLATE_NOOP("Constant", "Status"),
315
+ nwOutline.CCOUNT: QT_TRANSLATE_NOOP("Constant", "Chars"),
316
+ nwOutline.WCOUNT: QT_TRANSLATE_NOOP("Constant", "Words"),
317
+ nwOutline.PCOUNT: QT_TRANSLATE_NOOP("Constant", "Pars"),
318
+ nwOutline.POV: QT_TRANSLATE_NOOP("Constant", "POV"),
319
+ nwOutline.FOCUS: QT_TRANSLATE_NOOP("Constant", "Focus"),
320
+ nwOutline.CHAR: KEY_NAME[nwKeyWords.CHAR_KEY],
321
+ nwOutline.PLOT: KEY_NAME[nwKeyWords.PLOT_KEY],
322
+ nwOutline.WORLD: KEY_NAME[nwKeyWords.WORLD_KEY],
323
+ nwOutline.TIME: KEY_NAME[nwKeyWords.TIME_KEY],
324
+ nwOutline.OBJECT: KEY_NAME[nwKeyWords.OBJECT_KEY],
325
+ nwOutline.ENTITY: KEY_NAME[nwKeyWords.ENTITY_KEY],
326
+ nwOutline.CUSTOM: KEY_NAME[nwKeyWords.CUSTOM_KEY],
327
+ nwOutline.STORY: KEY_NAME[nwKeyWords.STORY_KEY],
328
+ nwOutline.MENTION: KEY_NAME[nwKeyWords.MENTION_KEY],
329
+ nwOutline.SYNOP: QT_TRANSLATE_NOOP("Constant", "Synopsis"),
330
+ }
331
+ STATS_NAME = {
332
+ nwStats.CHARS_ALL: QT_TRANSLATE_NOOP("Constant", "Characters"),
333
+ nwStats.CHARS_TEXT: QT_TRANSLATE_NOOP("Constant", "Characters in Text"),
334
+ nwStats.CHARS_TITLE: QT_TRANSLATE_NOOP("Constant", "Characters in Headings"),
335
+ nwStats.PARAGRAPHS: QT_TRANSLATE_NOOP("Constant", "Paragraphs"),
336
+ nwStats.TITLES: QT_TRANSLATE_NOOP("Constant", "Headings"),
337
+ nwStats.WCHARS_ALL: QT_TRANSLATE_NOOP("Constant", "Characters, No Spaces"),
338
+ nwStats.WCHARS_TEXT: QT_TRANSLATE_NOOP("Constant", "Characters in Text, No Spaces"),
339
+ nwStats.WCHARS_TITLE: QT_TRANSLATE_NOOP("Constant", "Characters in Headings, No Spaces"),
340
+ nwStats.WORDS_ALL: QT_TRANSLATE_NOOP("Constant", "Words"),
341
+ nwStats.WORDS_TEXT: QT_TRANSLATE_NOOP("Constant", "Words in Text"),
342
+ nwStats.WORDS_TITLE: QT_TRANSLATE_NOOP("Constant", "Words in Headings"),
244
343
  }
245
344
  BUILD_FMT = {
246
345
  nwBuildFmt.ODT: QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)"),
247
346
  nwBuildFmt.FODT: QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)"),
248
- nwBuildFmt.HTML: QT_TRANSLATE_NOOP("Constant", "novelWriter HTML (.html)"),
347
+ nwBuildFmt.DOCX: QT_TRANSLATE_NOOP("Constant", "Microsoft Word Document (.docx)"),
348
+ nwBuildFmt.HTML: QT_TRANSLATE_NOOP("Constant", "HTML 5 (.html)"),
249
349
  nwBuildFmt.NWD: QT_TRANSLATE_NOOP("Constant", "novelWriter Markup (.txt)"),
250
350
  nwBuildFmt.STD_MD: QT_TRANSLATE_NOOP("Constant", "Standard Markdown (.md)"),
251
351
  nwBuildFmt.EXT_MD: QT_TRANSLATE_NOOP("Constant", "Extended Markdown (.md)"),
252
- nwBuildFmt.J_HTML: QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter HTML (.json)"),
352
+ nwBuildFmt.PDF: QT_TRANSLATE_NOOP("Constant", "Portable Document Format (.pdf)"),
353
+ nwBuildFmt.J_HTML: QT_TRANSLATE_NOOP("Constant", "JSON + HTML 5 (.json)"),
253
354
  nwBuildFmt.J_NWD: QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter Markup (.json)"),
254
355
  }
255
356
  BUILD_EXT = {
256
357
  nwBuildFmt.ODT: ".odt",
257
358
  nwBuildFmt.FODT: ".fodt",
359
+ nwBuildFmt.DOCX: ".docx",
258
360
  nwBuildFmt.HTML: ".html",
259
361
  nwBuildFmt.NWD: ".txt",
260
362
  nwBuildFmt.STD_MD: ".md",
261
363
  nwBuildFmt.EXT_MD: ".md",
364
+ nwBuildFmt.PDF: ".pdf",
262
365
  nwBuildFmt.J_HTML: ".json",
263
366
  nwBuildFmt.J_NWD: ".json",
264
367
  }
@@ -343,11 +446,11 @@ class nwHeadFmt:
343
446
  CHAR_POV, CHAR_FOCUS
344
447
  ]
345
448
 
346
- # ODT Document Page Header
347
- ODT_PROJECT = "{Project}"
348
- ODT_AUTHOR = "{Author}"
349
- ODT_PAGE = "{Page}"
350
- ODT_AUTO = "{Project} / {Author} / {Page}"
449
+ # Document Page Header
450
+ DOC_PROJECT = "{Project}"
451
+ DOC_AUTHOR = "{Author}"
452
+ DOC_PAGE = "{Page}"
453
+ DOC_AUTO = "{Project} / {Author} / {Page}"
351
454
 
352
455
 
353
456
  class nwQuotes:
@@ -449,6 +552,11 @@ class nwUnicode:
449
552
  U_LTRI = "\u25c0" # Left-pointing triangle
450
553
  U_LTRIS = "\u25c2" # Left-pointing triangle, small
451
554
 
555
+ # Special
556
+ U_UNKN = "\ufffd" # Unknown character
557
+ U_NAC1 = "\ufffe" # Not a character
558
+ U_NAC2 = "\uffff" # Not a character
559
+
452
560
  # HTML Equivalents
453
561
  # ================
454
562