novelWriter 2.2b1__py3-none-any.whl → 2.2.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 (134) hide show
  1. {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/METADATA +3 -3
  2. {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/RECORD +128 -114
  3. {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +10 -5
  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_zh_CN.qm +0 -0
  13. novelwriter/assets/i18n/project_de_DE.json +1 -0
  14. novelwriter/assets/i18n/project_en_GB.json +1 -0
  15. novelwriter/assets/i18n/project_en_US.json +1 -0
  16. novelwriter/assets/i18n/project_es_419.json +11 -0
  17. novelwriter/assets/i18n/project_fr_FR.json +11 -0
  18. novelwriter/assets/i18n/project_it_IT.json +11 -0
  19. novelwriter/assets/i18n/project_ja_JP.json +2 -1
  20. novelwriter/assets/i18n/project_nb_NO.json +1 -0
  21. novelwriter/assets/i18n/project_zh_CN.json +11 -0
  22. novelwriter/assets/icons/novelwriter.ico +0 -0
  23. novelwriter/assets/icons/typicons_dark/icons.conf +11 -3
  24. novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +4 -0
  25. novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +4 -0
  26. novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +4 -0
  27. novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +4 -0
  28. novelwriter/assets/icons/typicons_dark/nw_panel.svg +4 -0
  29. novelwriter/assets/icons/typicons_dark/nw_tb-bold-md.svg +4 -0
  30. novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +3 -1
  31. novelwriter/assets/icons/typicons_dark/nw_tb-italic-md.svg +4 -0
  32. novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +3 -1
  33. novelwriter/assets/icons/typicons_dark/nw_tb-strike-md.svg +4 -0
  34. novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +3 -1
  35. novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +4 -2
  36. novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +4 -2
  37. novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +4 -2
  38. novelwriter/assets/icons/typicons_dark/typ_eye.svg +4 -0
  39. novelwriter/assets/icons/typicons_light/icons.conf +11 -3
  40. novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +4 -0
  41. novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +4 -0
  42. novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +4 -0
  43. novelwriter/assets/icons/typicons_light/nw_deco-note.svg +4 -0
  44. novelwriter/assets/icons/typicons_light/nw_panel.svg +4 -0
  45. novelwriter/assets/icons/typicons_light/nw_tb-bold-md.svg +4 -0
  46. novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +3 -1
  47. novelwriter/assets/icons/typicons_light/nw_tb-italic-md.svg +4 -0
  48. novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +3 -1
  49. novelwriter/assets/icons/typicons_light/nw_tb-strike-md.svg +4 -0
  50. novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +3 -1
  51. novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +4 -2
  52. novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +4 -2
  53. novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +4 -2
  54. novelwriter/assets/icons/typicons_light/typ_eye.svg +4 -0
  55. novelwriter/assets/icons/x-novelwriter-project.ico +0 -0
  56. novelwriter/assets/manual.pdf +0 -0
  57. novelwriter/assets/sample.zip +0 -0
  58. novelwriter/assets/text/release_notes.htm +50 -7
  59. novelwriter/common.py +35 -27
  60. novelwriter/config.py +13 -28
  61. novelwriter/constants.py +21 -4
  62. novelwriter/core/buildsettings.py +2 -2
  63. novelwriter/core/coretools.py +8 -2
  64. novelwriter/core/docbuild.py +1 -1
  65. novelwriter/core/document.py +1 -1
  66. novelwriter/core/index.py +102 -36
  67. novelwriter/core/item.py +2 -2
  68. novelwriter/core/options.py +6 -3
  69. novelwriter/core/project.py +5 -5
  70. novelwriter/core/projectdata.py +3 -3
  71. novelwriter/core/projectxml.py +1 -1
  72. novelwriter/core/sessions.py +2 -2
  73. novelwriter/core/spellcheck.py +4 -3
  74. novelwriter/core/status.py +3 -3
  75. novelwriter/core/storage.py +1 -1
  76. novelwriter/core/tohtml.py +11 -5
  77. novelwriter/core/tokenizer.py +28 -21
  78. novelwriter/core/tomd.py +6 -2
  79. novelwriter/core/toodt.py +12 -5
  80. novelwriter/core/tree.py +2 -2
  81. novelwriter/dialogs/about.py +30 -31
  82. novelwriter/dialogs/docmerge.py +24 -15
  83. novelwriter/dialogs/docsplit.py +27 -16
  84. novelwriter/dialogs/editlabel.py +19 -7
  85. novelwriter/dialogs/preferences.py +116 -131
  86. novelwriter/dialogs/projdetails.py +29 -36
  87. novelwriter/dialogs/projload.py +32 -36
  88. novelwriter/dialogs/projsettings.py +20 -15
  89. novelwriter/dialogs/quotes.py +32 -25
  90. novelwriter/dialogs/updates.py +17 -16
  91. novelwriter/dialogs/wordlist.py +34 -21
  92. novelwriter/enum.py +19 -8
  93. novelwriter/error.py +1 -1
  94. novelwriter/extensions/circularprogress.py +1 -1
  95. novelwriter/extensions/configlayout.py +3 -15
  96. novelwriter/extensions/{wheeleventfilter.py → eventfilters.py} +15 -5
  97. novelwriter/extensions/novelselector.py +1 -1
  98. novelwriter/extensions/pageddialog.py +1 -1
  99. novelwriter/extensions/pagedsidebar.py +2 -5
  100. novelwriter/extensions/simpleprogress.py +8 -9
  101. novelwriter/extensions/statusled.py +1 -1
  102. novelwriter/extensions/switch.py +4 -4
  103. novelwriter/extensions/switchbox.py +1 -6
  104. novelwriter/gui/doceditor.py +349 -236
  105. novelwriter/gui/dochighlight.py +10 -11
  106. novelwriter/gui/docviewer.py +158 -360
  107. novelwriter/gui/docviewerpanel.py +502 -0
  108. novelwriter/gui/editordocument.py +4 -4
  109. novelwriter/gui/itemdetails.py +2 -2
  110. novelwriter/gui/mainmenu.py +50 -36
  111. novelwriter/gui/noveltree.py +44 -53
  112. novelwriter/gui/outline.py +12 -7
  113. novelwriter/gui/projtree.py +465 -381
  114. novelwriter/gui/sidebar.py +9 -7
  115. novelwriter/gui/statusbar.py +48 -5
  116. novelwriter/gui/theme.py +26 -8
  117. novelwriter/guimain.py +212 -208
  118. novelwriter/shared.py +76 -30
  119. novelwriter/tools/dictionaries.py +268 -0
  120. novelwriter/tools/lipsum.py +34 -28
  121. novelwriter/tools/manusbuild.py +20 -10
  122. novelwriter/tools/manuscript.py +20 -27
  123. novelwriter/tools/manussettings.py +2 -4
  124. novelwriter/tools/projwizard.py +3 -3
  125. novelwriter/tools/writingstats.py +18 -5
  126. novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +0 -8
  127. novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +0 -8
  128. novelwriter/assets/icons/typicons_dark/typ_at.svg +0 -4
  129. novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +0 -8
  130. novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +0 -8
  131. novelwriter/assets/icons/typicons_light/typ_at.svg +0 -4
  132. {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/LICENSE.md +0 -0
  133. {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/entry_points.txt +0 -0
  134. {novelWriter-2.2b1.dist-info → novelWriter-2.2.1.dist-info}/top_level.txt +0 -0
novelwriter/common.py CHANGED
@@ -3,10 +3,10 @@ novelWriter – Common Functions
3
3
  ==============================
4
4
 
5
5
  File History:
6
- Created: 2019-05-12 [0.1]
6
+ Created: 2019-05-12 [0.1.0]
7
7
 
8
8
  This file is a part of novelWriter
9
- Copyright 2018–2023, Veronica Berglyd Olsen
9
+ Copyright 2018–2024, Veronica Berglyd Olsen
10
10
 
11
11
  This program is free software: you can redistribute it and/or modify
12
12
  it under the terms of the GNU General Public License as published by
@@ -33,9 +33,11 @@ from typing import Any, Literal
33
33
  from pathlib import Path
34
34
  from datetime import datetime
35
35
  from configparser import ConfigParser
36
+ from urllib.parse import urljoin
37
+ from urllib.request import pathname2url
36
38
 
37
- from PyQt5.QtCore import QCoreApplication
38
- from PyQt5.QtWidgets import QWidget, qApp
39
+ from PyQt5.QtGui import QDesktopServices
40
+ from PyQt5.QtCore import QCoreApplication, QUrl
39
41
 
40
42
  from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
41
43
  from novelwriter.error import logException
@@ -44,9 +46,9 @@ from novelwriter.constants import nwConst, nwUnicode
44
46
  logger = logging.getLogger(__name__)
45
47
 
46
48
 
47
- # =============================================================================================== #
49
+ ##
48
50
  # Checker Functions
49
- # =============================================================================================== #
51
+ ##
50
52
 
51
53
  def checkStringNone(value: Any, default: str | None) -> str | None:
52
54
  """Check if a variable is a string or a None."""
@@ -129,9 +131,9 @@ def checkPath(value: Any, default: Path) -> Path:
129
131
  return default
130
132
 
131
133
 
132
- # =============================================================================================== #
134
+ ##
133
135
  # Validator Functions
134
- # =============================================================================================== #
136
+ ##
135
137
 
136
138
  def isHandle(value: Any) -> bool:
137
139
  """Check if a string is a valid novelWriter handle.
@@ -202,9 +204,9 @@ def checkIntTuple(value: int, valid: tuple | list | set, default: int) -> int:
202
204
  return default
203
205
 
204
206
 
205
- # =============================================================================================== #
207
+ ##
206
208
  # Formatting Functions
207
- # =============================================================================================== #
209
+ ##
208
210
 
209
211
  def formatInt(value: int) -> str:
210
212
  """Formats an integer with k, M, G etc."""
@@ -248,9 +250,9 @@ def formatTime(t: int) -> str:
248
250
  return "ERROR"
249
251
 
250
252
 
251
- # =============================================================================================== #
253
+ ##
252
254
  # String Functions
253
- # =============================================================================================== #
255
+ ##
254
256
 
255
257
  def simplified(text: str) -> str:
256
258
  """Take a string and strip leading and trailing whitespaces, and
@@ -369,9 +371,9 @@ def numberToRoman(value: int, toLower: bool = False) -> str:
369
371
  return roman.lower() if toLower else roman
370
372
 
371
373
 
372
- # =============================================================================================== #
374
+ ##
373
375
  # Encoder Functions
374
- # =============================================================================================== #
376
+ ##
375
377
 
376
378
  def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
377
379
  """Encode a dictionary, list or tuple as a json object or array, and
@@ -461,9 +463,9 @@ def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
461
463
  return
462
464
 
463
465
 
464
- # =============================================================================================== #
466
+ ##
465
467
  # File and File System Functions
466
- # =============================================================================================== #
468
+ ##
467
469
 
468
470
  def readTextFile(path: str | Path) -> str:
469
471
  """Read the content of a text file in a robust manner."""
@@ -487,21 +489,27 @@ def makeFileNameSafe(text: str) -> str:
487
489
  return "".join(c for c in text if c.isalnum() or c in allowed)
488
490
 
489
491
 
490
- # =============================================================================================== #
491
- # Other Functions
492
- # =============================================================================================== #
492
+ def getFileSize(path: Path) -> int:
493
+ """Return the size of a file."""
494
+ try:
495
+ return path.stat().st_size
496
+ except Exception:
497
+ return -1
493
498
 
494
- def getGuiItem(objName: str) -> QWidget | None:
495
- """Returns a QtWidget based on its objectName."""
496
- for qWidget in qApp.topLevelWidgets():
497
- if qWidget.objectName() == objName:
498
- return qWidget
499
- return None
499
+
500
+ def openExternalPath(path: Path) -> bool:
501
+ """Open a path by passing it to the desktop environment."""
502
+ if Path(path).exists():
503
+ QDesktopServices.openUrl(
504
+ QUrl(urljoin("file:", pathname2url(str(path))))
505
+ )
506
+ return True
507
+ return False
500
508
 
501
509
 
502
- # =============================================================================================== #
510
+ ##
503
511
  # Classes
504
- # =============================================================================================== #
512
+ ##
505
513
 
506
514
  class NWConfigParser(ConfigParser):
507
515
  """Common: Adapted Config Parser
novelwriter/config.py CHANGED
@@ -7,7 +7,7 @@ Created: 2018-09-22 [0.0.1] Config
7
7
  Created: 2022-11-09 [2.0rc2] RecentProjects
8
8
 
9
9
  This file is a part of novelWriter
10
- Copyright 2018–2023, Veronica Berglyd Olsen
10
+ Copyright 2018–2024, Veronica Berglyd Olsen
11
11
 
12
12
  This program is free software: you can redistribute it and/or modify
13
13
  it under the terms of the GNU General Public License as published by
@@ -148,6 +148,7 @@ class Config:
148
148
 
149
149
  self.autoScroll = False # Typewriter-like scrolling
150
150
  self.autoScrollPos = 30 # Start point for typewriter-like scrolling
151
+ self.scrollPastEnd = True # Scroll past end of document, and centre cursor
151
152
 
152
153
  self.wordCountTimer = 5.0 # Interval for word count update in seconds
153
154
  self.incNotesWCount = True # The status bar word count includes notes
@@ -178,9 +179,9 @@ class Config:
178
179
  self.spellLanguage = "en"
179
180
 
180
181
  # State
181
- self.showRefPanel = True # The reference panel for the viewer is visible
182
+ self.showViewerPanel = True # The panel for the viewer is visible
182
183
  self.showEditToolBar = False # The document editor toolbar visibility
183
- self.useShortcodes = False # Use shorcodes for basic formatting
184
+ self.useShortcodes = False # Use shortcodes for basic formatting
184
185
  self.viewComments = True # Comments are shown in the viewer
185
186
  self.viewSynopsis = True # Synopsis is shown in the viewer
186
187
 
@@ -224,6 +225,8 @@ class Config:
224
225
  # Other System Info
225
226
  self.hostName = QSysInfo.machineHostName()
226
227
  self.kernelVer = QSysInfo.kernelVersion()
228
+ self.isDebug = False # True if running in debug mode
229
+ self.memInfo = False # True if displaying mem info in status bar
227
230
 
228
231
  # Packages
229
232
  self.hasEnchant = False # The pyenchant package
@@ -391,8 +394,7 @@ class Config:
391
394
  return self._appPath / "assets"
392
395
 
393
396
  def lastPath(self) -> Path:
394
- """Return the last path used by the user, but ensure it exists.
395
- """
397
+ """Return the last path used by the user, if it exists."""
396
398
  if isinstance(self._lastPath, Path):
397
399
  if self._lastPath.is_dir():
398
400
  return self._lastPath
@@ -400,9 +402,8 @@ class Config:
400
402
 
401
403
  def backupPath(self) -> Path:
402
404
  """Return the backup path."""
403
- if isinstance(self._backupPath, Path):
404
- if self._backupPath.is_dir():
405
- return self._backupPath
405
+ if isinstance(self._backupPath, Path) and self._backupPath.is_dir():
406
+ return self._backupPath
406
407
  return self._backPath
407
408
 
408
409
  def errorText(self) -> str:
@@ -577,6 +578,7 @@ class Config:
577
578
  self.doReplaceDots = conf.rdBool(sec, "repdots", self.doReplaceDots)
578
579
  self.autoScroll = conf.rdBool(sec, "autoscroll", self.autoScroll)
579
580
  self.autoScrollPos = conf.rdInt(sec, "autoscrollpos", self.autoScrollPos)
581
+ self.scrollPastEnd = conf.rdBool(sec, "scrollpastend", self.scrollPastEnd)
580
582
  self.fmtSQuoteOpen = conf.rdStr(sec, "fmtsquoteopen", self.fmtSQuoteOpen)
581
583
  self.fmtSQuoteClose = conf.rdStr(sec, "fmtsquoteclose", self.fmtSQuoteClose)
582
584
  self.fmtDQuoteOpen = conf.rdStr(sec, "fmtdquoteopen", self.fmtDQuoteOpen)
@@ -600,7 +602,7 @@ class Config:
600
602
 
601
603
  # State
602
604
  sec = "State"
603
- self.showRefPanel = conf.rdBool(sec, "showrefpanel", self.showRefPanel)
605
+ self.showViewerPanel = conf.rdBool(sec, "showviewerpanel", self.showViewerPanel)
604
606
  self.showEditToolBar = conf.rdBool(sec, "showedittoolbar", self.showEditToolBar)
605
607
  self.useShortcodes = conf.rdBool(sec, "useshortcodes", self.useShortcodes)
606
608
  self.viewComments = conf.rdBool(sec, "viewcomments", self.viewComments)
@@ -612,24 +614,6 @@ class Config:
612
614
  self.searchNextFile = conf.rdBool(sec, "searchnextfile", self.searchNextFile)
613
615
  self.searchMatchCap = conf.rdBool(sec, "searchmatchcap", self.searchMatchCap)
614
616
 
615
- # Deprecated Settings or Locations as of 2.0
616
- # ToDo: These will be loaded for a few minor releases until the users have converted them
617
- self.guiFont = conf.rdStr("Main", "guifont", self.guiFont)
618
- self.guiFontSize = conf.rdInt("Main", "guifontsize", self.guiFontSize)
619
- self.guiLocale = conf.rdStr("Main", "guilang", self.guiLocale)
620
- self._backupPath = conf.rdPath("Backup", "backuppath", self._backupPath)
621
- self.backupOnClose = conf.rdBool("Backup", "backuponclose", self.backupOnClose)
622
- self.askBeforeBackup = conf.rdBool("Backup", "askbeforebackup", self.askBeforeBackup)
623
- fmtSingleQuotes = conf.rdStrList(sec, "fmtsinglequote", [])
624
- fmtDoubleQuotes = conf.rdStrList(sec, "fmtdoublequote", [])
625
-
626
- if isinstance(fmtSingleQuotes, list) and len(fmtSingleQuotes) == 2:
627
- self.fmtSQuoteOpen = fmtSingleQuotes[0]
628
- self.fmtSQuoteClose = fmtSingleQuotes[1]
629
- if isinstance(fmtDoubleQuotes, list) and len(fmtDoubleQuotes) == 2:
630
- self.fmtDQuoteOpen = fmtDoubleQuotes[0]
631
- self.fmtDQuoteClose = fmtDoubleQuotes[1]
632
-
633
617
  # Check Values
634
618
  # ============
635
619
 
@@ -701,6 +685,7 @@ class Config:
701
685
  "repdots": str(self.doReplaceDots),
702
686
  "autoscroll": str(self.autoScroll),
703
687
  "autoscrollpos": str(self.autoScrollPos),
688
+ "scrollpastend": str(self.scrollPastEnd),
704
689
  "fmtsquoteopen": str(self.fmtSQuoteOpen),
705
690
  "fmtsquoteclose": str(self.fmtSQuoteClose),
706
691
  "fmtdquoteopen": str(self.fmtDQuoteOpen),
@@ -724,7 +709,7 @@ class Config:
724
709
  }
725
710
 
726
711
  conf["State"] = {
727
- "showrefpanel": str(self.showRefPanel),
712
+ "showviewerpanel": str(self.showViewerPanel),
728
713
  "showedittoolbar": str(self.showEditToolBar),
729
714
  "useshortcodes": str(self.useShortcodes),
730
715
  "viewcomments": str(self.viewComments),
novelwriter/constants.py CHANGED
@@ -6,7 +6,7 @@ File History:
6
6
  Created: 2019-04-28 [0.0.1]
7
7
 
8
8
  This file is a part of novelWriter
9
- Copyright 2018–2023, Veronica Berglyd Olsen
9
+ Copyright 2018–2024, Veronica Berglyd Olsen
10
10
 
11
11
  This program is free software: you can redistribute it and/or modify
12
12
  it under the terms of the GNU General Public License as published by
@@ -50,6 +50,9 @@ class nwConst:
50
50
  URL_HELP = "https://github.com/vkbo/novelWriter/discussions"
51
51
  URL_RELEASE = "https://github.com/vkbo/novelWriter/releases/latest"
52
52
 
53
+ # Requests
54
+ USER_AGENT = "Mozilla/5.0 (compatible; novelWriter (Python))"
55
+
53
56
  # Gui Settings
54
57
  STATUS_MSG_TIMEOUT = 15000 # milliseconds
55
58
 
@@ -153,6 +156,21 @@ class nwKeyWords:
153
156
  # END Class nwKeyWords
154
157
 
155
158
 
159
+ class nwLists:
160
+
161
+ USER_CLASSES = [
162
+ nwItemClass.CHARACTER,
163
+ nwItemClass.PLOT,
164
+ nwItemClass.WORLD,
165
+ nwItemClass.TIMELINE,
166
+ nwItemClass.OBJECT,
167
+ nwItemClass.ENTITY,
168
+ nwItemClass.CUSTOM,
169
+ ]
170
+
171
+ # END Class nwLists
172
+
173
+
156
174
  class nwLabels:
157
175
 
158
176
  CLASS_NAME = {
@@ -221,8 +239,8 @@ class nwLabels:
221
239
  nwOutline.FOCUS: QT_TRANSLATE_NOOP("Constant", "Focus"),
222
240
  nwOutline.CHAR: KEY_NAME[nwKeyWords.CHAR_KEY],
223
241
  nwOutline.PLOT: KEY_NAME[nwKeyWords.PLOT_KEY],
224
- nwOutline.TIME: KEY_NAME[nwKeyWords.TIME_KEY],
225
242
  nwOutline.WORLD: KEY_NAME[nwKeyWords.WORLD_KEY],
243
+ nwOutline.TIME: KEY_NAME[nwKeyWords.TIME_KEY],
226
244
  nwOutline.OBJECT: KEY_NAME[nwKeyWords.OBJECT_KEY],
227
245
  nwOutline.ENTITY: KEY_NAME[nwKeyWords.ENTITY_KEY],
228
246
  nwOutline.CUSTOM: KEY_NAME[nwKeyWords.CUSTOM_KEY],
@@ -327,8 +345,7 @@ class nwQuotes:
327
345
 
328
346
 
329
347
  class nwUnicode:
330
- """Supported unicode character constants and their HTML equivalents.
331
- """
348
+ """Supported unicode character constants and their HTML equivalents."""
332
349
  # Unicode Constants
333
350
  # =================
334
351
 
@@ -7,7 +7,7 @@ Created: 2023-02-14 [2.1b1] BuildSettings
7
7
  Created: 2023-05-22 [2.1b1] BuildCollection
8
8
 
9
9
  This file is a part of novelWriter
10
- Copyright 2018–2023, Veronica Berglyd Olsen
10
+ Copyright 2018–2024, Veronica Berglyd Olsen
11
11
 
12
12
  This program is free software: you can redistribute it and/or modify
13
13
  it under the terms of the GNU General Public License as published by
@@ -349,7 +349,7 @@ class BuildSettings:
349
349
  def buildItemFilter(
350
350
  self, project: NWProject, withRoots: bool = False
351
351
  ) -> dict[str, tuple[bool, FilterMode]]:
352
- """Return a dictionary of item handles with filter decissions
352
+ """Return a dictionary of item handles with filter decisions
353
353
  applied.
354
354
  """
355
355
  result: dict[str, tuple[bool, FilterMode]] = {}
@@ -6,9 +6,10 @@ File History:
6
6
  Created: 2022-10-02 [2.0rc1] DocMerger
7
7
  Created: 2022-10-11 [2.0rc1] DocSplitter
8
8
  Created: 2022-11-03 [2.0rc2] ProjectBuilder
9
+ Created: 2023-07-20 [2.1b1] DocDuplicator
9
10
 
10
11
  This file is a part of novelWriter
11
- Copyright 2018–2023, Veronica Berglyd Olsen
12
+ Copyright 2018–2024, Veronica Berglyd Olsen
12
13
 
13
14
  This program is free software: you can redistribute it and/or modify
14
15
  it under the terms of the GNU General Public License as published by
@@ -406,6 +407,7 @@ class ProjectBuilder:
406
407
 
407
408
  chSynop = self.tr("Summary of the chapter.")
408
409
  scSynop = self.tr("Summary of the scene.")
410
+ bfNote = self.tr("A short description.")
409
411
 
410
412
  # Create chapters
411
413
  if numChapters > 0:
@@ -446,7 +448,11 @@ class ProjectBuilder:
446
448
  aHandle = project.newFile(noteTitles[newRoot], rHandle)
447
449
  ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
448
450
  aDoc = project.storage.getDocument(aHandle)
449
- aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n")
451
+ aDoc.writeDocument(
452
+ f"# {noteTitles[newRoot]}\n\n"
453
+ f"@tag: {ntTag}\n\n"
454
+ f"% Short: {bfNote}\n\n"
455
+ )
450
456
 
451
457
  # Also add the archive and trash folders
452
458
  project.newRoot(nwItemClass.ARCHIVE)
@@ -6,7 +6,7 @@ File History:
6
6
  Created: 2022-12-01 [2.1b1] NWBuildDocument
7
7
 
8
8
  This file is a part of novelWriter
9
- Copyright 2018–2023, Veronica Berglyd Olsen
9
+ Copyright 2018–2024, Veronica Berglyd Olsen
10
10
 
11
11
  This program is free software: you can redistribute it and/or modify
12
12
  it under the terms of the GNU General Public License as published by
@@ -6,7 +6,7 @@ File History:
6
6
  Created: 2018-09-29 [0.0.1]
7
7
 
8
8
  This file is a part of novelWriter
9
- Copyright 2018–2023, Veronica Berglyd Olsen
9
+ Copyright 2018–2024, Veronica Berglyd Olsen
10
10
 
11
11
  This program is free software: you can redistribute it and/or modify
12
12
  it under the terms of the GNU General Public License as published by
novelwriter/core/index.py CHANGED
@@ -11,7 +11,7 @@ Created: 2022-05-29 [2.0rc1] TagsIndex
11
11
  Created: 2022-05-29 [2.0rc1] ItemIndex
12
12
 
13
13
  This file is a part of novelWriter
14
- Copyright 2018–2023, Veronica Berglyd Olsen
14
+ Copyright 2018–2024, Veronica Berglyd Olsen
15
15
 
16
16
  This program is free software: you can redistribute it and/or modify
17
17
  it under the terms of the GNU General Public License as published by
@@ -35,7 +35,8 @@ from time import time
35
35
  from typing import TYPE_CHECKING, ItemsView, Iterable, Iterator
36
36
  from pathlib import Path
37
37
 
38
- from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
38
+ from novelwriter import SHARED
39
+ from novelwriter.enum import nwComment, nwItemClass, nwItemType, nwItemLayout
39
40
  from novelwriter.error import logException
40
41
  from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
41
42
  from novelwriter.constants import nwFiles, nwKeyWords, nwRegEx, nwUnicode, nwHeaders
@@ -112,6 +113,7 @@ class NWIndex:
112
113
  self._itemIndex.clear()
113
114
  self._indexChange = 0.0
114
115
  self._rootChange = {}
116
+ SHARED.indexSignalProxy({"event": "clearIndex"})
115
117
  return
116
118
 
117
119
  def rebuildIndex(self) -> None:
@@ -121,16 +123,22 @@ class NWIndex:
121
123
  if nwItem.isFileType():
122
124
  tHandle = nwItem.itemHandle
123
125
  theDoc = self._project.storage.getDocument(tHandle)
124
- self.scanText(tHandle, theDoc.readDocument() or "")
126
+ self.scanText(tHandle, theDoc.readDocument() or "", blockSignal=True)
125
127
  self._indexBroken = False
128
+ SHARED.indexSignalProxy({"event": "buildIndex"})
126
129
  return
127
130
 
128
131
  def deleteHandle(self, tHandle: str) -> None:
129
132
  """Delete all entries of a given document handle."""
130
133
  logger.debug("Removing item '%s' from the index", tHandle)
131
- for tTag in self._itemIndex.allItemTags(tHandle):
134
+ delTags = self._itemIndex.allItemTags(tHandle)
135
+ for tTag in delTags:
132
136
  del self._tagsIndex[tTag]
133
137
  del self._itemIndex[tHandle]
138
+ SHARED.indexSignalProxy({
139
+ "event": "updateTags",
140
+ "deleted": delTags,
141
+ })
134
142
  return
135
143
 
136
144
  def reIndexHandle(self, tHandle: str | None) -> bool:
@@ -138,14 +146,12 @@ class NWIndex:
138
146
  moved from the archive or trash folders back into the active
139
147
  project.
140
148
  """
141
- if tHandle is None or not self._project.tree.checkType(tHandle, nwItemType.FILE):
142
- return False
143
-
144
- logger.debug("Re-indexing item '%s'", tHandle)
145
- theDoc = self._project.storage.getDocument(tHandle)
146
- self.scanText(tHandle, theDoc.readDocument() or "")
147
-
148
- return True
149
+ if tHandle and self._project.tree.checkType(tHandle, nwItemType.FILE):
150
+ logger.debug("Re-indexing item '%s'", tHandle)
151
+ theDoc = self._project.storage.getDocument(tHandle)
152
+ self.scanText(tHandle, theDoc.readDocument() or "")
153
+ return True
154
+ return False
149
155
 
150
156
  def indexChangedSince(self, checkTime: int | float) -> bool:
151
157
  """Check if the index has changed since a given time."""
@@ -200,6 +206,7 @@ class NWIndex:
200
206
  self.reIndexHandle(fHandle)
201
207
 
202
208
  self._indexChange = time()
209
+ SHARED.indexSignalProxy({"event": "buildIndex"})
203
210
 
204
211
  logger.debug("Index loaded in %.3f ms", (time() - tStart)*1000)
205
212
 
@@ -238,7 +245,7 @@ class NWIndex:
238
245
  # Index Building
239
246
  ##
240
247
 
241
- def scanText(self, tHandle: str, text: str) -> bool:
248
+ def scanText(self, tHandle: str, text: str, blockSignal: bool = False) -> bool:
242
249
  """Scan a piece of text associated with a handle. This will
243
250
  update the indices accordingly. This function takes the handle
244
251
  and text as separate inputs as we want to primarily scan the
@@ -282,6 +289,11 @@ class NWIndex:
282
289
  nowTime = time()
283
290
  self._indexChange = nowTime
284
291
  self._rootChange[tItem.itemRoot] = nowTime
292
+ if not blockSignal:
293
+ SHARED.indexSignalProxy({
294
+ "event": "scanText",
295
+ "handle": tHandle,
296
+ })
285
297
 
286
298
  return True
287
299
 
@@ -289,7 +301,7 @@ class NWIndex:
289
301
  # Internal Indexer Helpers
290
302
  ##
291
303
 
292
- def _scanActive(self, tHandle: str, nwItem: NWItem, text: str, tags: dict) -> None:
304
+ def _scanActive(self, tHandle: str, nwItem: NWItem, text: str, tags: dict[str, bool]) -> None:
293
305
  """Scan an active document for meta data."""
294
306
  nTitle = 0 # Line Number of the previous title
295
307
  cTitle = TT_NONE # Tag of the current title
@@ -326,14 +338,9 @@ class NWIndex:
326
338
 
327
339
  elif line.startswith("%"):
328
340
  if cTitle != TT_NONE:
329
- toCheck = line[1:].lstrip()
330
- synTag = toCheck[:9].lower()
331
- tLen = len(line)
332
- cLen = len(toCheck)
333
- cOff = tLen - cLen
334
- if synTag == "synopsis:":
335
- sText = line[cOff+9:].strip()
336
- self._itemIndex.setHeadingSynopsis(tHandle, cTitle, sText)
341
+ cStyle, cText, _ = processComment(line)
342
+ if cStyle in (nwComment.SYNOPSIS, nwComment.SHORT):
343
+ self._itemIndex.setHeadingSynopsis(tHandle, cTitle, cText)
337
344
 
338
345
  # Count words for remaining text after last heading
339
346
  if pTitle != TT_NONE:
@@ -346,9 +353,21 @@ class NWIndex:
346
353
 
347
354
  # Prune no longer used tags
348
355
  for tTag, isActive in tags.items():
349
- if not isActive:
350
- logger.debug("Deleting removed tag '%s'", tTag)
356
+ updated = []
357
+ deleted = []
358
+ if isActive:
359
+ logger.debug("Added/updated tag '%s'", tTag)
360
+ updated.append(tTag)
361
+ else:
362
+ logger.debug("Removed tag '%s'", tTag)
351
363
  del self._tagsIndex[tTag]
364
+ deleted.append(tTag)
365
+ if updated or deleted:
366
+ SHARED.indexSignalProxy({
367
+ "event": "updateTags",
368
+ "updated": updated,
369
+ "deleted": deleted,
370
+ })
352
371
 
353
372
  return
354
373
 
@@ -385,7 +404,7 @@ class NWIndex:
385
404
  return
386
405
 
387
406
  def _indexKeyword(self, tHandle: str, line: str, sTitle: str,
388
- itemClass: nwItemClass, tags: dict) -> None:
407
+ itemClass: nwItemClass, tags: dict[str, bool]) -> None:
389
408
  """Validate and save the information about a reference to a tag
390
409
  in another file, or the setting of a tag in the file. A record
391
410
  of active tags is updated so that no longer used tags can be
@@ -532,7 +551,7 @@ class NWIndex:
532
551
  return 0
533
552
 
534
553
  def getTableOfContents(
535
- self, rHandle: str, maxDepth: int, skipExcl: bool = True
554
+ self, rHandle: str | None, maxDepth: int, skipExcl: bool = True
536
555
  ) -> list[tuple[str, int, str, int]]:
537
556
  """Generate a table of contents up to a maximum depth."""
538
557
  tOrder = []
@@ -596,10 +615,8 @@ class NWIndex:
596
615
 
597
616
  return tRefs
598
617
 
599
- def getBackReferenceList(self, tHandle: str) -> dict[str, str]:
600
- """Build a list of files referring back to our file, specified
601
- by tHandle.
602
- """
618
+ def getBackReferenceList(self, tHandle: str) -> dict[str, tuple[str, IndexHeading]]:
619
+ """Build a dict of files referring back to our file."""
603
620
  if tHandle is None or tHandle not in self._itemIndex:
604
621
  return {}
605
622
 
@@ -611,20 +628,43 @@ class NWIndex:
611
628
  for aHandle, sTitle, hItem in self._itemIndex.iterAllHeaders():
612
629
  for aTag in hItem.references:
613
630
  if aTag in tTags and aHandle not in tRefs:
614
- tRefs[aHandle] = sTitle
631
+ tRefs[aHandle] = (sTitle, hItem)
615
632
 
616
633
  return tRefs
617
634
 
618
- def getTagSource(self, tagKey: str) -> tuple[str, str]:
635
+ def getTagSource(self, tagKey: str) -> tuple[str | None, str]:
619
636
  """Return the source location of a given tag."""
620
637
  tHandle = self._tagsIndex.tagHandle(tagKey)
621
638
  sTitle = self._tagsIndex.tagHeading(tagKey)
622
639
  return tHandle, sTitle
623
640
 
624
- def getTags(self, itemClass: nwItemClass) -> list[str]:
641
+ def getDocumentTags(self, tHandle: str | None) -> list[str]:
642
+ """Return all tags used by a specific document."""
643
+ return self._itemIndex.allItemTags(tHandle) if tHandle else []
644
+
645
+ def getClassTags(self, itemClass: nwItemClass) -> list[str]:
625
646
  """Return all tags based on itemClass."""
626
647
  return self._tagsIndex.filterTagNames(itemClass.name)
627
648
 
649
+ def getTagsData(self) -> Iterator[tuple[str, str, str, IndexItem | None, IndexHeading | None]]:
650
+ """Return all known tags."""
651
+ for tag, data in self._tagsIndex.items():
652
+ iItem = self._itemIndex[data.get("handle")]
653
+ hItem = None if iItem is None else iItem[data.get("heading")]
654
+ yield tag, data.get("name", ""), data.get("class", ""), iItem, hItem
655
+ return
656
+
657
+ def getSingleTag(self, tagKey: str) -> tuple[str, str, IndexItem | None, IndexHeading | None]:
658
+ """Return tag data for a specific tag."""
659
+ tName = self._tagsIndex.tagName(tagKey)
660
+ tClass = self._tagsIndex.tagClass(tagKey)
661
+ tHandle = self._tagsIndex.tagHandle(tagKey)
662
+ tHeading = self._tagsIndex.tagHeading(tagKey)
663
+ if tName and tClass and tHandle and tHeading:
664
+ iItem = self._itemIndex[tHandle]
665
+ return tName, tClass, iItem, None if iItem is None else iItem[tHeading]
666
+ return "", "", None, None
667
+
628
668
  # END Class NWIndex
629
669
 
630
670
 
@@ -643,7 +683,7 @@ class TagsIndex:
643
683
  __slots__ = ("_tags")
644
684
 
645
685
  def __init__(self) -> None:
646
- self._tags: dict[str, dict] = {}
686
+ self._tags: dict[str, dict[str, str]] = {}
647
687
  return
648
688
 
649
689
  def __contains__(self, tagKey: str) -> bool:
@@ -665,6 +705,10 @@ class TagsIndex:
665
705
  self._tags = {}
666
706
  return
667
707
 
708
+ def items(self) -> ItemsView:
709
+ """Return a dictionary view of all tags."""
710
+ return self._tags.items()
711
+
668
712
  def add(self, tagKey: str, tHandle: str, sTitle: str, itemClass: nwItemClass) -> None:
669
713
  """Add a key to the index and set all values."""
670
714
  self._tags[tagKey.lower()] = {
@@ -676,7 +720,7 @@ class TagsIndex:
676
720
  """Get the display name of a given tag."""
677
721
  return self._tags.get(tagKey.lower(), {}).get("name", "")
678
722
 
679
- def tagHandle(self, tagKey: str) -> str:
723
+ def tagHandle(self, tagKey: str) -> str | None:
680
724
  """Get the handle of a given tag."""
681
725
  return self._tags.get(tagKey.lower(), {}).get("handle", None)
682
726
 
@@ -937,6 +981,11 @@ class IndexItem:
937
981
  # Properties
938
982
  ##
939
983
 
984
+ @property
985
+ def handle(self) -> str:
986
+ """Return the item handle of the index item."""
987
+ return self._handle
988
+
940
989
  @property
941
990
  def item(self) -> NWItem:
942
991
  """Return the project item of the index item."""
@@ -1215,9 +1264,26 @@ class IndexHeading:
1215
1264
 
1216
1265
 
1217
1266
  # =============================================================================================== #
1218
- # Simple Word Counter
1267
+ # Text Processing Functions
1219
1268
  # =============================================================================================== #
1220
1269
 
1270
+ CLASSIFIERS = {
1271
+ "short": nwComment.SHORT,
1272
+ "synopsis": nwComment.SYNOPSIS,
1273
+ }
1274
+
1275
+
1276
+ def processComment(text: str) -> tuple[nwComment, str, int]:
1277
+ """Extract comment style and text. Should only be called on text
1278
+ starting with a %.
1279
+ """
1280
+ check = text[1:].lstrip()
1281
+ classifier, _, content = check.partition(":")
1282
+ if content and (clean := classifier.strip().lower()) in CLASSIFIERS:
1283
+ return CLASSIFIERS[clean], content.strip(), text.find(":") + 1
1284
+ return nwComment.PLAIN, check, 0
1285
+
1286
+
1221
1287
  def countWords(text: str) -> tuple[int, int, int]:
1222
1288
  """Count words in a piece of text, skipping special syntax and
1223
1289
  comments.