novelWriter 2.6b1__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 (68) hide show
  1. {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/METADATA +3 -3
  2. {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/RECORD +68 -52
  3. {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +49 -10
  5. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  6. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  7. novelwriter/assets/i18n/nw_ru_RU.qm +0 -0
  8. novelwriter/assets/i18n/project_de_DE.json +2 -2
  9. novelwriter/assets/i18n/project_ru_RU.json +11 -0
  10. novelwriter/assets/icons/typicons_dark/icons.conf +7 -0
  11. novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
  12. novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
  13. novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
  14. novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
  15. novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
  16. novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
  17. novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
  18. novelwriter/assets/icons/typicons_light/icons.conf +7 -0
  19. novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
  20. novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
  21. novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
  22. novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
  23. novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
  24. novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
  25. novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
  26. novelwriter/assets/manual.pdf +0 -0
  27. novelwriter/assets/sample.zip +0 -0
  28. novelwriter/assets/text/credits_en.htm +1 -0
  29. novelwriter/common.py +37 -2
  30. novelwriter/config.py +15 -12
  31. novelwriter/constants.py +24 -9
  32. novelwriter/core/coretools.py +111 -125
  33. novelwriter/core/docbuild.py +3 -2
  34. novelwriter/core/index.py +9 -19
  35. novelwriter/core/item.py +39 -6
  36. novelwriter/core/itemmodel.py +518 -0
  37. novelwriter/core/project.py +67 -89
  38. novelwriter/core/status.py +7 -5
  39. novelwriter/core/tree.py +268 -287
  40. novelwriter/dialogs/docmerge.py +7 -17
  41. novelwriter/dialogs/preferences.py +3 -3
  42. novelwriter/dialogs/projectsettings.py +2 -2
  43. novelwriter/enum.py +7 -0
  44. novelwriter/extensions/configlayout.py +6 -4
  45. novelwriter/formats/todocx.py +34 -38
  46. novelwriter/formats/tohtml.py +14 -15
  47. novelwriter/formats/tokenizer.py +21 -17
  48. novelwriter/formats/toodt.py +53 -124
  49. novelwriter/formats/toqdoc.py +92 -44
  50. novelwriter/gui/doceditor.py +230 -219
  51. novelwriter/gui/docviewer.py +38 -9
  52. novelwriter/gui/docviewerpanel.py +14 -22
  53. novelwriter/gui/itemdetails.py +17 -24
  54. novelwriter/gui/mainmenu.py +13 -8
  55. novelwriter/gui/noveltree.py +12 -12
  56. novelwriter/gui/outline.py +10 -11
  57. novelwriter/gui/projtree.py +548 -1202
  58. novelwriter/gui/search.py +9 -10
  59. novelwriter/gui/theme.py +7 -3
  60. novelwriter/guimain.py +59 -43
  61. novelwriter/shared.py +52 -23
  62. novelwriter/text/patterns.py +17 -5
  63. novelwriter/tools/manusbuild.py +13 -11
  64. novelwriter/tools/manussettings.py +42 -52
  65. novelwriter/types.py +7 -1
  66. {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/LICENSE.md +0 -0
  67. {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/entry_points.txt +0 -0
  68. {novelWriter-2.6b1.dist-info → novelWriter-2.6b2.dist-info}/top_level.txt +0 -0
@@ -35,6 +35,7 @@ translators for the languages currently available:</p>
35
35
  <li><b>Norwegian:</b> Veronica Berglyd Olsen (vkbo)</li>
36
36
  <li><b>Polish:</b> Anna Maria Polak (Nauthiz)</li>
37
37
  <li><b>Portuguese:</b> Bruno Meneguello (bkmeneguello)</li>
38
+ <li><b>Russian:</b> Konstantin Tan (k1kimosha)</li>
38
39
  <li><b>Simplified Chinese:</b> Qianzhi Long (longqzh)</li>
39
40
  </ul>
40
41
 
novelwriter/common.py CHANGED
@@ -37,8 +37,8 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar
37
37
  from urllib.parse import urljoin
38
38
  from urllib.request import pathname2url
39
39
 
40
- from PyQt5.QtCore import QCoreApplication, QUrl
41
- 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
42
42
 
43
43
  from novelwriter.constants import nwConst, nwLabels, nwUnicode, trConst
44
44
  from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
@@ -434,6 +434,30 @@ def describeFont(font: QFont) -> str:
434
434
  return "Error"
435
435
 
436
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
+
437
461
  def qtLambda(func: Callable, *args: Any, **kwargs: Any) -> Callable:
438
462
  """A replacement for Python lambdas that works for Qt slots."""
439
463
  def wrapper(*a_: Any) -> None:
@@ -441,6 +465,17 @@ def qtLambda(func: Callable, *args: Any, **kwargs: Any) -> Callable:
441
465
  return wrapper
442
466
 
443
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
+
444
479
  ##
445
480
  # Encoder Functions
446
481
  ##
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
 
@@ -369,10 +372,11 @@ class Config:
369
372
  def setGuiFont(self, value: QFont | str | None) -> None:
370
373
  """Update the GUI's font style from settings."""
371
374
  if isinstance(value, QFont):
372
- self.guiFont = value
375
+ self.guiFont = fontMatcher(value)
373
376
  elif value and isinstance(value, str):
374
- self.guiFont = QFont()
375
- self.guiFont.fromString(value)
377
+ font = QFont()
378
+ font.fromString(value)
379
+ self.guiFont = fontMatcher(font)
376
380
  else:
377
381
  font = QFont()
378
382
  fontDB = QFontDatabase()
@@ -382,11 +386,9 @@ class Config:
382
386
  font.setPointSize(10)
383
387
  else:
384
388
  font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont)
385
- self.guiFont = font
389
+ self.guiFont = fontMatcher(font)
386
390
  logger.debug("GUI font set to: %s", describeFont(font))
387
-
388
391
  QApplication.setFont(self.guiFont)
389
-
390
392
  return
391
393
 
392
394
  def setTextFont(self, value: QFont | str | None) -> None:
@@ -394,10 +396,11 @@ class Config:
394
396
  set to default font.
395
397
  """
396
398
  if isinstance(value, QFont):
397
- self.textFont = value
399
+ self.textFont = fontMatcher(value)
398
400
  elif value and isinstance(value, str):
399
- self.textFont = QFont()
400
- self.textFont.fromString(value)
401
+ font = QFont()
402
+ font.fromString(value)
403
+ self.textFont = fontMatcher(font)
401
404
  else:
402
405
  fontDB = QFontDatabase()
403
406
  fontFam = fontDB.families()
@@ -411,8 +414,8 @@ class Config:
411
414
  font.setPointSize(12)
412
415
  else:
413
416
  font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont)
414
- self.textFont = font
415
- 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))
416
419
  return
417
420
 
418
421
  ##
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
@@ -105,6 +108,9 @@ class nwStyles:
105
108
  H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
106
109
  H_SIZES = {0: 2.50, 1: 2.00, 2: 1.75, 3: 1.50, 4: 1.25}
107
110
 
111
+ T_NORMAL = 1.0
112
+ T_SMALL = 0.8
113
+
108
114
  T_LABEL = {
109
115
  "H0": QT_TRANSLATE_NOOP("Constant", "Title"),
110
116
  "H1": QT_TRANSLATE_NOOP("Constant", "Heading 1 (Partition)"),
@@ -115,15 +121,15 @@ class nwStyles:
115
121
  "SP": QT_TRANSLATE_NOOP("Constant", "Scene Separator"),
116
122
  }
117
123
  T_MARGIN = {
118
- "H0": (1.42, 0.50), # Title margins
119
- "H1": (1.42, 0.50), # Heading 1 margins
120
- "H2": (1.67, 0.50), # Heading 2 margins
121
- "H3": (1.17, 0.50), # Heading 3 margins
122
- "H4": (1.17, 0.50), # Heading 4 margins
123
- "TT": (0.00, 0.58), # Text margins
124
- "SP": (1.17, 1.17), # Separator margins
125
- "MT": (0.00, 0.58), # Meta margins
126
- "FT": (1.42, 0.47), # Footnote margins
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)
127
133
  }
128
134
 
129
135
 
@@ -268,6 +274,10 @@ class nwLabels:
268
274
  "doc_h4": QT_TRANSLATE_NOOP("Constant", "Novel Section"),
269
275
  "note": QT_TRANSLATE_NOOP("Constant", "Project Note"),
270
276
  }
277
+ ACTIVE_NAME = {
278
+ "checked": QT_TRANSLATE_NOOP("Constant", "Active"),
279
+ "unchecked": QT_TRANSLATE_NOOP("Constant", "Inactive"),
280
+ }
271
281
  KEY_NAME = {
272
282
  nwKeyWords.TAG_KEY: QT_TRANSLATE_NOOP("Constant", "Tag"),
273
283
  nwKeyWords.POV_KEY: QT_TRANSLATE_NOOP("Constant", "Point of View"),
@@ -542,6 +552,11 @@ class nwUnicode:
542
552
  U_LTRI = "\u25c0" # Left-pointing triangle
543
553
  U_LTRIS = "\u25c2" # Left-pointing triangle, small
544
554
 
555
+ # Special
556
+ U_UNKN = "\ufffd" # Unknown character
557
+ U_NAC1 = "\ufffe" # Not a character
558
+ U_NAC2 = "\uffff" # Not a character
559
+
545
560
  # HTML Equivalents
546
561
  # ================
547
562
 
@@ -56,10 +56,17 @@ class DocMerger:
56
56
  def __init__(self, project: NWProject) -> None:
57
57
  self._project = project
58
58
  self._error = ""
59
- self._targetDoc = None
60
- self._targetText = []
59
+ self._target = None
60
+ self._text = []
61
61
  return
62
62
 
63
+ @property
64
+ def targetHandle(self) -> str | None:
65
+ """Get the handle of the target document."""
66
+ if self._target:
67
+ return self._target.itemHandle
68
+ return None
69
+
63
70
  ##
64
71
  # Methods
65
72
  ##
@@ -72,63 +79,56 @@ class DocMerger:
72
79
  """Set the target document for the merging. Calling this
73
80
  function resets the class.
74
81
  """
75
- self._targetDoc = tHandle
76
- self._targetText = []
82
+ self._target = self._project.tree[tHandle]
83
+ self._text = []
77
84
  return
78
85
 
79
- def newTargetDoc(self, srcHandle: str, docLabel: str) -> str | None:
86
+ def newTargetDoc(self, sHandle: str, label: str) -> None:
80
87
  """Create a brand new target document based on a source handle
81
88
  and a new doc label. Calling this function resets the class.
82
89
  """
83
- srcItem = self._project.tree[srcHandle]
84
- if srcItem is None or srcItem.itemParent is None:
85
- return None
86
-
87
- newHandle = self._project.newFile(docLabel, srcItem.itemParent)
88
- newItem = self._project.tree[newHandle]
89
- if isinstance(newItem, NWItem):
90
- newItem.setLayout(srcItem.itemLayout)
91
- newItem.setStatus(srcItem.itemStatus)
92
- newItem.setImport(srcItem.itemImport)
93
-
94
- self._targetDoc = newHandle
95
- self._targetText = []
96
-
97
- return newHandle
90
+ sItem = self._project.tree[sHandle]
91
+ if sItem and sItem.itemParent:
92
+ tHandle = self._project.newFile(label, sItem.itemParent)
93
+ if nwItem := self._project.tree[tHandle]:
94
+ nwItem.setLayout(sItem.itemLayout)
95
+ nwItem.setStatus(sItem.itemStatus)
96
+ nwItem.setImport(sItem.itemImport)
97
+ nwItem.notifyToRefresh()
98
+ self._target = nwItem
99
+ self._text = []
100
+ return
98
101
 
99
- def appendText(self, srcHandle: str, addComment: bool, cmtPrefix: str) -> bool:
102
+ def appendText(self, sHandle: str, addComment: bool, cmtPrefix: str) -> None:
100
103
  """Append text from an existing document to the text buffer."""
101
- srcItem = self._project.tree[srcHandle]
102
- if srcItem is None:
103
- return False
104
-
105
- docText = self._project.storage.getDocumentText(srcHandle).rstrip("\n")
106
- if addComment:
107
- docInfo = srcItem.describeMe()
108
- docSt, _ = srcItem.getImportStatus()
109
- cmtLine = f"% {cmtPrefix} {docInfo}: {srcItem.itemName} [{docSt}]\n\n"
110
- docText = cmtLine + docText
111
-
112
- self._targetText.append(docText)
113
-
114
- return True
104
+ if item := self._project.tree[sHandle]:
105
+ text = self._project.storage.getDocumentText(sHandle).rstrip("\n")
106
+ if addComment:
107
+ info = item.describeMe()
108
+ status, _ = item.getImportStatus()
109
+ text = f"% {cmtPrefix} {info}: {item.itemName} [{status}]\n\n{text}"
110
+ self._text.append(text)
111
+ return
115
112
 
116
113
  def writeTargetDoc(self) -> bool:
117
114
  """Write the accumulated text into the designated target
118
115
  document, appending any existing text.
119
116
  """
120
- if self._targetDoc is None:
121
- return False
117
+ if self._target:
118
+ outDoc = self._project.storage.getDocument(self._target.itemHandle)
119
+ if text := (outDoc.readDocument() or "").rstrip("\n"):
120
+ self._text.insert(0, text)
122
121
 
123
- outDoc = self._project.storage.getDocument(self._targetDoc)
124
- if text := (outDoc.readDocument() or "").rstrip("\n"):
125
- self._targetText.insert(0, text)
122
+ status = outDoc.writeDocument("\n\n".join(self._text) + "\n\n")
123
+ if not status:
124
+ self._error = outDoc.getError()
126
125
 
127
- status = outDoc.writeDocument("\n\n".join(self._targetText) + "\n\n")
128
- if not status:
129
- self._error = outDoc.getError()
126
+ self._project.index.reIndexHandle(self._target.itemHandle)
127
+ self._target.notifyToRefresh()
130
128
 
131
- return status
129
+ return status
130
+
131
+ return False
132
132
 
133
133
 
134
134
  class DocSplitter:
@@ -172,23 +172,19 @@ class DocSplitter:
172
172
  self._inFolder = False
173
173
  return
174
174
 
175
- def newParentFolder(self, pHandle: str, folderLabel: str) -> str | None:
175
+ def newParentFolder(self, pHandle: str, folderLabel: str) -> None:
176
176
  """Create a new folder that will be the top level parent item
177
177
  for the new documents.
178
178
  """
179
- if self._srcItem is None:
180
- return None
181
-
182
- newHandle = self._project.newFolder(folderLabel, pHandle)
183
- newItem = self._project.tree[newHandle]
184
- if isinstance(newItem, NWItem):
185
- newItem.setStatus(self._srcItem.itemStatus)
186
- newItem.setImport(self._srcItem.itemImport)
187
-
188
- self._parHandle = newHandle
189
- self._inFolder = True
190
-
191
- return newHandle
179
+ if self._srcItem:
180
+ nHandle = self._project.newFolder(folderLabel, pHandle)
181
+ if nwItem := self._project.tree[nHandle]:
182
+ nwItem.setStatus(self._srcItem.itemStatus)
183
+ nwItem.setImport(self._srcItem.itemImport)
184
+ nwItem.notifyToRefresh()
185
+ self._parHandle = nHandle
186
+ self._inFolder = True
187
+ return
192
188
 
193
189
  def splitDocument(self, splitData: list, splitText: list[str]) -> None:
194
190
  """Loop through the split data record and perform the split job
@@ -202,58 +198,50 @@ class DocSplitter:
202
198
  self._rawData.insert(0, (chunk, hLevel, hLabel))
203
199
  return
204
200
 
205
- def writeDocuments(self, docHierarchy: bool) -> Iterable[tuple[bool, str | None, str | None]]:
201
+ def writeDocuments(self, docHierarchy: bool) -> Iterable[bool]:
206
202
  """An iterator that will write each document in the buffer, and
207
203
  return its new handle, parent handle, and sibling handle.
208
204
  """
209
- if self._srcHandle is None or self._srcItem is None or self._parHandle is None:
210
- return
211
-
212
- pHandle = self._parHandle
213
- nHandle = self._parHandle if self._inFolder else self._srcHandle
214
- hHandle = [self._parHandle, None, None, None, None]
215
-
216
- pLevel = 0
217
- for docText, hLevel, docLabel in self._rawData:
218
-
219
- hLevel = minmax(hLevel, 1, 4)
220
- if pLevel == 0:
221
- pLevel = hLevel
222
-
223
- if docHierarchy:
224
- if hLevel == 1:
225
- pHandle = self._parHandle
226
- elif hLevel == 2:
227
- pHandle = hHandle[1] or hHandle[0]
228
- elif hLevel == 3:
229
- pHandle = hHandle[2] or hHandle[1] or hHandle[0]
230
- elif hLevel == 4:
231
- pHandle = hHandle[3] or hHandle[2] or hHandle[1] or hHandle[0]
232
-
233
- if hLevel < pLevel:
234
- nHandle = hHandle[hLevel] or hHandle[0]
235
- elif hLevel > pLevel:
236
- nHandle = pHandle
237
-
238
- dHandle = self._project.newFile(docLabel, pHandle)
239
- hHandle[hLevel] = dHandle
240
-
241
- newItem = self._project.tree[dHandle]
242
- if isinstance(newItem, NWItem):
243
- newItem.setStatus(self._srcItem.itemStatus)
244
- newItem.setImport(self._srcItem.itemImport)
245
-
246
- outDoc = self._project.storage.getDocument(dHandle)
247
- status = outDoc.writeDocument("\n".join(docText))
248
- if not status:
249
- self._error = outDoc.getError()
250
-
251
- yield status, dHandle, nHandle
252
-
253
- hHandle[hLevel] = dHandle
254
- nHandle = dHandle
255
- pLevel = hLevel
256
-
205
+ if self._srcHandle and self._srcItem and self._parHandle:
206
+ pHandle = self._parHandle
207
+ hHandle = [self._parHandle, None, None, None, None]
208
+ pLevel = 0
209
+ for docText, hLevel, docLabel in self._rawData:
210
+
211
+ hLevel = minmax(hLevel, 1, 4)
212
+ if pLevel == 0:
213
+ pLevel = hLevel
214
+
215
+ if docHierarchy:
216
+ if hLevel == 1:
217
+ pHandle = self._parHandle
218
+ elif hLevel == 2:
219
+ pHandle = hHandle[1] or hHandle[0]
220
+ elif hLevel == 3:
221
+ pHandle = hHandle[2] or hHandle[1] or hHandle[0]
222
+ elif hLevel == 4:
223
+ pHandle = hHandle[3] or hHandle[2] or hHandle[1] or hHandle[0]
224
+
225
+ if (
226
+ (dHandle := self._project.newFile(docLabel, pHandle))
227
+ and (nwItem := self._project.tree[dHandle])
228
+ ):
229
+ hHandle[hLevel] = dHandle
230
+ nwItem.setStatus(self._srcItem.itemStatus)
231
+ nwItem.setImport(self._srcItem.itemImport)
232
+
233
+ outDoc = self._project.storage.getDocument(dHandle)
234
+ status = outDoc.writeDocument("\n".join(docText))
235
+ if not status:
236
+ self._error = outDoc.getError()
237
+
238
+ self._project.index.reIndexHandle(dHandle)
239
+ nwItem.notifyToRefresh()
240
+
241
+ yield status
242
+
243
+ hHandle[hLevel] = dHandle
244
+ pLevel = hLevel
257
245
  return
258
246
 
259
247
 
@@ -270,29 +258,27 @@ class DocDuplicator:
270
258
  # Methods
271
259
  ##
272
260
 
273
- def duplicate(self, items: list[str]) -> Iterable[tuple[str, str | None]]:
261
+ def duplicate(self, items: list[str]) -> list[str]:
274
262
  """Run through a list of items, duplicate them, and copy the
275
263
  text content if they are documents.
276
264
  """
265
+ result = []
266
+ after = True
277
267
  if items:
278
- nHandle = items[0]
279
268
  hMap: dict[str, str | None] = {t: None for t in items}
280
269
  for tHandle in items:
281
- newItem = self._project.tree.duplicate(tHandle)
282
- if newItem is None:
283
- return
284
- hMap[tHandle] = newItem.itemHandle
285
- if newItem.itemParent in hMap:
286
- newItem.setParent(hMap[newItem.itemParent])
287
- self._project.tree.updateItemData(newItem.itemHandle)
288
- if newItem.isFileType():
289
- newDoc = self._project.storage.getDocument(newItem.itemHandle)
290
- if newDoc.fileExists():
291
- return
292
- newDoc.writeDocument(self._project.storage.getDocumentText(tHandle))
293
- yield newItem.itemHandle, nHandle
294
- nHandle = None
295
- return
270
+ if oldItem := self._project.tree[tHandle]:
271
+ pHandle = hMap.get(oldItem.itemParent or "") or oldItem.itemParent
272
+ if newItem := self._project.tree.duplicate(tHandle, pHandle, after):
273
+ hMap[tHandle] = newItem.itemHandle
274
+ if newItem.isFileType():
275
+ self._project.copyFileContent(newItem.itemHandle, tHandle)
276
+ newItem.notifyToRefresh()
277
+ result.append(newItem.itemHandle)
278
+ after = False
279
+ else:
280
+ break
281
+ return result
296
282
 
297
283
 
298
284
  class DocSearch:
@@ -523,7 +509,7 @@ class ProjectBuilder:
523
509
 
524
510
  # Also add the archive and trash folders
525
511
  project.newRoot(nwItemClass.ARCHIVE)
526
- project.trashFolder()
512
+ project.tree.trash # Triggers the creation of Trash
527
513
 
528
514
  project.saveProject()
529
515
  project.closeProject()
@@ -176,8 +176,9 @@ class NWBuildDocument:
176
176
 
177
177
  elif bFormat == nwBuildFmt.PDF:
178
178
  makeObj = ToQTextDocument(self._project)
179
+ makeObj.disableAnchors()
179
180
  filtered = self._setupBuild(makeObj)
180
- makeObj.initDocument()
181
+ makeObj.initDocument(pdf=True)
181
182
  yield from self._iterBuild(makeObj, filtered)
182
183
  makeObj.closeDocument()
183
184
 
@@ -217,7 +218,7 @@ class NWBuildDocument:
217
218
  textFont = QFont(CONFIG.textFont)
218
219
  textFont.fromString(self._build.getStr("format.textFont"))
219
220
 
220
- bldObj.setFont(textFont)
221
+ bldObj.setTextFont(textFont)
221
222
  bldObj.setLanguage(self._project.data.language)
222
223
 
223
224
  bldObj.setPartitionFormat(
novelwriter/core/index.py CHANGED
@@ -116,24 +116,24 @@ class NWIndex:
116
116
  # Public Methods
117
117
  ##
118
118
 
119
- def clearIndex(self) -> None:
119
+ def clear(self) -> None:
120
120
  """Clear the index dictionaries and time stamps."""
121
121
  self._tagsIndex.clear()
122
122
  self._itemIndex.clear()
123
123
  self._indexChange = 0.0
124
124
  self._rootChange = {}
125
- SHARED.indexSignalProxy({"event": "clearIndex"})
125
+ SHARED.emitIndexCleared(self._project)
126
126
  return
127
127
 
128
- def rebuildIndex(self) -> None:
128
+ def rebuild(self) -> None:
129
129
  """Rebuild the entire index from scratch."""
130
- self.clearIndex()
130
+ self.clear()
131
131
  for nwItem in self._project.tree:
132
132
  if nwItem.isFileType():
133
133
  text = self._project.storage.getDocumentText(nwItem.itemHandle)
134
134
  self.scanText(nwItem.itemHandle, text, blockSignal=True)
135
135
  self._indexBroken = False
136
- SHARED.indexSignalProxy({"event": "buildIndex"})
136
+ SHARED.emitIndexAvailable(self._project)
137
137
  return
138
138
 
139
139
  def deleteHandle(self, tHandle: str) -> None:
@@ -143,10 +143,7 @@ class NWIndex:
143
143
  for tTag in delTags:
144
144
  del self._tagsIndex[tTag]
145
145
  del self._itemIndex[tHandle]
146
- SHARED.indexSignalProxy({
147
- "event": "updateTags",
148
- "deleted": delTags,
149
- })
146
+ SHARED.emitIndexChangedTags(self._project, [], delTags)
150
147
  return
151
148
 
152
149
  def reIndexHandle(self, tHandle: str | None) -> None:
@@ -212,7 +209,7 @@ class NWIndex:
212
209
  self.reIndexHandle(fHandle)
213
210
 
214
211
  self._indexChange = time()
215
- SHARED.indexSignalProxy({"event": "buildIndex"})
212
+ SHARED.emitIndexAvailable(self._project)
216
213
 
217
214
  logger.debug("Index loaded in %.3f ms", (time() - tStart)*1000)
218
215
 
@@ -296,10 +293,7 @@ class NWIndex:
296
293
  self._indexChange = nowTime
297
294
  self._rootChange[tItem.itemRoot] = nowTime
298
295
  if not blockSignal:
299
- SHARED.indexSignalProxy({
300
- "event": "scanText",
301
- "handle": tHandle,
302
- })
296
+ tItem.notifyToRefresh()
303
297
 
304
298
  return True
305
299
 
@@ -370,11 +364,7 @@ class NWIndex:
370
364
  del self._tagsIndex[tTag]
371
365
  deleted.append(tTag)
372
366
  if updated or deleted:
373
- SHARED.indexSignalProxy({
374
- "event": "updateTags",
375
- "updated": updated,
376
- "deleted": deleted,
377
- })
367
+ SHARED.emitIndexChangedTags(self._project, updated, deleted)
378
368
 
379
369
  return
380
370