novelWriter 2.2rc1__py3-none-any.whl → 2.3b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/RECORD +141 -129
  3. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +11 -6
  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_US.json +1 -0
  15. novelwriter/assets/i18n/project_es_419.json +11 -0
  16. novelwriter/assets/i18n/project_fr_FR.json +11 -0
  17. novelwriter/assets/i18n/project_it_IT.json +11 -0
  18. novelwriter/assets/i18n/project_ja_JP.json +2 -1
  19. novelwriter/assets/i18n/project_nb_NO.json +1 -0
  20. novelwriter/assets/i18n/project_zh_CN.json +11 -0
  21. novelwriter/assets/icons/typicons_dark/icons.conf +9 -2
  22. novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
  23. novelwriter/assets/icons/typicons_dark/nw_tb-bold-md.svg +4 -0
  24. novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +3 -1
  25. novelwriter/assets/icons/typicons_dark/nw_tb-italic-md.svg +4 -0
  26. novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +3 -1
  27. novelwriter/assets/icons/typicons_dark/nw_tb-strike-md.svg +4 -0
  28. novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +3 -1
  29. novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +4 -2
  30. novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +4 -2
  31. novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +4 -2
  32. novelwriter/assets/icons/typicons_dark/typ_document-add-col.svg +8 -0
  33. novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
  34. novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
  35. novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
  36. novelwriter/assets/icons/typicons_light/icons.conf +9 -2
  37. novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
  38. novelwriter/assets/icons/typicons_light/nw_tb-bold-md.svg +4 -0
  39. novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +3 -1
  40. novelwriter/assets/icons/typicons_light/nw_tb-italic-md.svg +4 -0
  41. novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +3 -1
  42. novelwriter/assets/icons/typicons_light/nw_tb-strike-md.svg +4 -0
  43. novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +3 -1
  44. novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +4 -2
  45. novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +4 -2
  46. novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +4 -2
  47. novelwriter/assets/icons/typicons_light/typ_document-add-col.svg +8 -0
  48. novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
  49. novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
  50. novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
  51. novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
  52. novelwriter/assets/images/novelwriter-text-light.svg +4 -0
  53. novelwriter/assets/images/welcome-dark.jpg +0 -0
  54. novelwriter/assets/images/welcome-light.jpg +0 -0
  55. novelwriter/assets/manual.pdf +0 -0
  56. novelwriter/assets/sample.zip +0 -0
  57. novelwriter/assets/syntax/default_dark.conf +1 -0
  58. novelwriter/assets/syntax/default_light.conf +1 -0
  59. novelwriter/assets/syntax/grey_dark.conf +1 -0
  60. novelwriter/assets/syntax/grey_light.conf +1 -0
  61. novelwriter/assets/syntax/light_owl.conf +1 -0
  62. novelwriter/assets/syntax/night_owl.conf +1 -0
  63. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  64. novelwriter/assets/syntax/solarized_light.conf +1 -0
  65. novelwriter/assets/syntax/tomorrow.conf +1 -0
  66. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  67. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  68. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  69. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  70. novelwriter/assets/text/credits_en.htm +4 -2
  71. novelwriter/assets/themes/default_dark.conf +2 -2
  72. novelwriter/assets/themes/default_light.conf +2 -2
  73. novelwriter/common.py +64 -66
  74. novelwriter/config.py +39 -44
  75. novelwriter/constants.py +39 -17
  76. novelwriter/core/buildsettings.py +8 -8
  77. novelwriter/core/coretools.py +194 -155
  78. novelwriter/core/docbuild.py +7 -4
  79. novelwriter/core/document.py +7 -7
  80. novelwriter/core/index.py +90 -57
  81. novelwriter/core/item.py +23 -5
  82. novelwriter/core/options.py +11 -10
  83. novelwriter/core/project.py +72 -47
  84. novelwriter/core/projectdata.py +3 -16
  85. novelwriter/core/projectxml.py +14 -42
  86. novelwriter/core/sessions.py +4 -3
  87. novelwriter/core/spellcheck.py +6 -4
  88. novelwriter/core/status.py +5 -4
  89. novelwriter/core/storage.py +179 -141
  90. novelwriter/core/tohtml.py +6 -4
  91. novelwriter/core/tokenizer.py +74 -46
  92. novelwriter/core/tomd.py +2 -2
  93. novelwriter/core/toodt.py +41 -31
  94. novelwriter/core/tree.py +5 -4
  95. novelwriter/dialogs/about.py +88 -179
  96. novelwriter/dialogs/docmerge.py +30 -20
  97. novelwriter/dialogs/docsplit.py +33 -22
  98. novelwriter/dialogs/editlabel.py +20 -8
  99. novelwriter/dialogs/preferences.py +562 -725
  100. novelwriter/dialogs/{projsettings.py → projectsettings.py} +301 -270
  101. novelwriter/dialogs/quotes.py +47 -36
  102. novelwriter/dialogs/wordlist.py +128 -59
  103. novelwriter/enum.py +25 -22
  104. novelwriter/error.py +2 -2
  105. novelwriter/extensions/circularprogress.py +12 -12
  106. novelwriter/extensions/configlayout.py +185 -146
  107. novelwriter/extensions/{wheeleventfilter.py → eventfilters.py} +15 -5
  108. novelwriter/extensions/modified.py +81 -0
  109. novelwriter/extensions/novelselector.py +27 -13
  110. novelwriter/extensions/pagedsidebar.py +15 -20
  111. novelwriter/extensions/simpleprogress.py +8 -9
  112. novelwriter/extensions/statusled.py +9 -9
  113. novelwriter/extensions/switch.py +32 -64
  114. novelwriter/extensions/switchbox.py +2 -7
  115. novelwriter/extensions/versioninfo.py +153 -0
  116. novelwriter/gui/doceditor.py +250 -214
  117. novelwriter/gui/dochighlight.py +66 -94
  118. novelwriter/gui/docviewer.py +71 -98
  119. novelwriter/gui/docviewerpanel.py +140 -47
  120. novelwriter/gui/editordocument.py +3 -3
  121. novelwriter/gui/itemdetails.py +9 -9
  122. novelwriter/gui/mainmenu.py +47 -46
  123. novelwriter/gui/noveltree.py +53 -61
  124. novelwriter/gui/outline.py +100 -76
  125. novelwriter/gui/projtree.py +193 -67
  126. novelwriter/gui/sidebar.py +9 -8
  127. novelwriter/gui/statusbar.py +49 -7
  128. novelwriter/gui/theme.py +65 -74
  129. novelwriter/guimain.py +173 -330
  130. novelwriter/shared.py +68 -30
  131. novelwriter/tools/dictionaries.py +7 -8
  132. novelwriter/tools/lipsum.py +34 -28
  133. novelwriter/tools/manusbuild.py +3 -4
  134. novelwriter/tools/manuscript.py +25 -32
  135. novelwriter/tools/manussettings.py +194 -225
  136. novelwriter/tools/noveldetails.py +525 -0
  137. novelwriter/tools/welcome.py +802 -0
  138. novelwriter/tools/writingstats.py +26 -13
  139. novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +0 -8
  140. novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +0 -8
  141. novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +0 -8
  142. novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +0 -8
  143. novelwriter/assets/images/wizard-back.jpg +0 -0
  144. novelwriter/assets/text/gplv3_en.htm +0 -641
  145. novelwriter/assets/text/release_notes.htm +0 -17
  146. novelwriter/dialogs/projdetails.py +0 -525
  147. novelwriter/dialogs/projload.py +0 -298
  148. novelwriter/dialogs/updates.py +0 -182
  149. novelwriter/extensions/pageddialog.py +0 -130
  150. novelwriter/tools/projwizard.py +0 -478
  151. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/LICENSE.md +0 -0
  152. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/entry_points.txt +0 -0
  153. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/top_level.txt +0 -0
@@ -8,7 +8,7 @@ Created: 2022-06-12 [2.0rc1] GuiNovelView
8
8
  Created: 2022-06-12 [2.0rc1] GuiNovelToolBar
9
9
 
10
10
  This file is a part of novelWriter
11
- Copyright 2018–2020, Veronica Berglyd Olsen
11
+ Copyright 2018–2024, Veronica Berglyd Olsen
12
12
 
13
13
  This program is free software: you can redistribute it and/or modify
14
14
  it under the terms of the GNU General Public License as published by
@@ -145,6 +145,7 @@ class GuiNovelView(QWidget):
145
145
  """Run closing project tasks."""
146
146
  lastColType = self.novelTree.lastColType
147
147
  lastColSize = self.novelTree.lastColSize
148
+ logger.debug("Saving State: GuiNovelView")
148
149
  pOptions = SHARED.project.options
149
150
  pOptions.setValue("GuiNovelView", "lastCol", lastColType)
150
151
  pOptions.setValue("GuiNovelView", "lastColSize", lastColSize)
@@ -205,18 +206,19 @@ class GuiNovelToolBar(QWidget):
205
206
 
206
207
  # Novel Selector
207
208
  selFont = self.font()
208
- selFont.setWeight(QFont.Bold)
209
- self.novelPrefix = self.tr("Outline of {0}")
209
+ selFont.setWeight(QFont.Weight.Bold)
210
+
210
211
  self.novelValue = NovelSelector(self)
211
212
  self.novelValue.setFont(selFont)
213
+ self.novelValue.setListFormat(self.tr("Outline of {0}"))
212
214
  self.novelValue.setMinimumWidth(CONFIG.pxInt(150))
213
- self.novelValue.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
215
+ self.novelValue.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
214
216
  self.novelValue.novelSelectionChanged.connect(self.setCurrentRoot)
215
217
 
216
218
  self.tbNovel = QToolButton(self)
217
219
  self.tbNovel.setToolTip(self.tr("Novel Root"))
218
220
  self.tbNovel.setIconSize(QSize(iPx, iPx))
219
- self.tbNovel.clicked.connect(self._openNovelSelector)
221
+ self.tbNovel.clicked.connect(self.novelValue.showPopup)
220
222
 
221
223
  # Refresh Button
222
224
  self.tbRefresh = QToolButton(self)
@@ -243,7 +245,7 @@ class GuiNovelToolBar(QWidget):
243
245
  self.tbMore.setToolTip(self.tr("More Options"))
244
246
  self.tbMore.setIconSize(QSize(iPx, iPx))
245
247
  self.tbMore.setMenu(self.mMore)
246
- self.tbMore.setPopupMode(QToolButton.InstantPopup)
248
+ self.tbMore.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
247
249
 
248
250
  # Assemble
249
251
  self.outerBox = QHBoxLayout()
@@ -274,7 +276,7 @@ class GuiNovelToolBar(QWidget):
274
276
  self.tbMore.setIcon(SHARED.theme.getIcon("menu"))
275
277
 
276
278
  qPalette = self.palette()
277
- qPalette.setBrush(QPalette.Window, qPalette.base())
279
+ qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
278
280
  self.setPalette(qPalette)
279
281
 
280
282
  # StyleSheets
@@ -293,7 +295,7 @@ class GuiNovelToolBar(QWidget):
293
295
  "QComboBox {border-style: none; padding-left: 0;} "
294
296
  "QComboBox::drop-down {border-style: none}"
295
297
  )
296
- self.novelValue.updateList(prefix=self.novelPrefix)
298
+ self.novelValue.refreshNovelList()
297
299
  self.tbNovel.setVisible(self.novelValue.count() > 1)
298
300
 
299
301
  return
@@ -306,7 +308,7 @@ class GuiNovelToolBar(QWidget):
306
308
 
307
309
  def buildNovelRootMenu(self) -> None:
308
310
  """Build the novel root menu."""
309
- self.novelValue.updateList(prefix=self.novelPrefix)
311
+ self.novelValue.refreshNovelList()
310
312
  self.tbNovel.setVisible(self.novelValue.count() > 1)
311
313
  return
312
314
 
@@ -326,12 +328,6 @@ class GuiNovelToolBar(QWidget):
326
328
  # Private Slots
327
329
  ##
328
330
 
329
- @pyqtSlot()
330
- def _openNovelSelector(self) -> None:
331
- """Trigger the dropdown list of the novel selector."""
332
- self.novelValue.showPopup()
333
- return
334
-
335
331
  @pyqtSlot()
336
332
  def _refreshNovelTree(self) -> None:
337
333
  """Rebuild the current tree."""
@@ -407,14 +403,14 @@ class GuiNovelTree(QTreeWidget):
407
403
  cMg = CONFIG.pxInt(6)
408
404
 
409
405
  self.setIconSize(QSize(iPx, iPx))
410
- self.setFrameStyle(QFrame.NoFrame)
406
+ self.setFrameStyle(QFrame.Shape.NoFrame)
411
407
  self.setUniformRowHeights(True)
412
408
  self.setAllColumnsShowFocus(True)
413
409
  self.setHeaderHidden(True)
414
410
  self.setIndentation(0)
415
411
  self.setColumnCount(4)
416
- self.setSelectionBehavior(QAbstractItemView.SelectRows)
417
- self.setSelectionMode(QAbstractItemView.SingleSelection)
412
+ self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
413
+ self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
418
414
  self.setExpandsOnDoubleClick(False)
419
415
  self.setDragEnabled(False)
420
416
 
@@ -422,10 +418,10 @@ class GuiNovelTree(QTreeWidget):
422
418
  treeHeader = self.header()
423
419
  treeHeader.setStretchLastSection(False)
424
420
  treeHeader.setMinimumSectionSize(iPx + cMg)
425
- treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.Stretch)
426
- treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents)
427
- treeHeader.setSectionResizeMode(self.C_EXTRA, QHeaderView.ResizeToContents)
428
- treeHeader.setSectionResizeMode(self.C_MORE, QHeaderView.ResizeToContents)
421
+ treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.ResizeMode.Stretch)
422
+ treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeMode.ResizeToContents)
423
+ treeHeader.setSectionResizeMode(self.C_EXTRA, QHeaderView.ResizeMode.ResizeToContents)
424
+ treeHeader.setSectionResizeMode(self.C_MORE, QHeaderView.ResizeMode.ResizeToContents)
429
425
 
430
426
  # Pre-Generate Tree Formatting
431
427
  fH1 = self.font()
@@ -454,14 +450,14 @@ class GuiNovelTree(QTreeWidget):
454
450
  """Set or update tree widget settings."""
455
451
  # Scroll bars
456
452
  if CONFIG.hideVScroll:
457
- self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
453
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
458
454
  else:
459
- self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
455
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
460
456
 
461
457
  if CONFIG.hideHScroll:
462
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
458
+ self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
463
459
  else:
464
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
460
+ self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
465
461
 
466
462
  return
467
463
 
@@ -521,21 +517,16 @@ class GuiNovelTree(QTreeWidget):
521
517
 
522
518
  def refreshHandle(self, tHandle: str) -> None:
523
519
  """Refresh the data for a given handle."""
524
- idxData = SHARED.project.index.getItemData(tHandle)
525
- if idxData is None:
526
- return
527
-
528
- logger.debug("Refreshing meta data for item '%s'", tHandle)
529
- for sTitle, tHeading in idxData.items():
530
- sKey = f"{tHandle}:{sTitle}"
531
- trItem = self._treeMap.get(sKey, None)
532
- if trItem is None:
533
- logger.debug("Heading '%s' not in novel tree", sKey)
534
- self.refreshTree()
535
- return
536
-
537
- self._updateTreeItemValues(trItem, tHeading, tHandle, sTitle)
538
-
520
+ if idxData := SHARED.project.index.getItemData(tHandle):
521
+ logger.debug("Refreshing meta data for item '%s'", tHandle)
522
+ for sTitle, tHeading in idxData.items():
523
+ sKey = f"{tHandle}:{sTitle}"
524
+ if trItem := self._treeMap.get(sKey, None):
525
+ self._updateTreeItemValues(trItem, tHeading, tHandle, sTitle)
526
+ else:
527
+ logger.debug("Heading '%s' not in novel tree", sKey)
528
+ self.refreshTree()
529
+ return
539
530
  return
540
531
 
541
532
  def getSelectedHandle(self) -> tuple[str | None, str | None]:
@@ -566,27 +557,25 @@ class GuiNovelTree(QTreeWidget):
566
557
  self._lastColSize = minmax(colSize, 15, 75)/100.0
567
558
  return
568
559
 
569
- def setActiveHandle(self, tHandle: str | None) -> None:
560
+ def setActiveHandle(self, tHandle: str | None, doScroll: bool = False) -> None:
570
561
  """Highlight the rows associated with a given handle."""
571
- tStart = time()
572
-
562
+ didScroll = False
573
563
  self._actHandle = tHandle
574
564
  for i in range(self.topLevelItemCount()):
575
- tItem = self.topLevelItem(i)
576
- if tItem is not None:
565
+ if tItem := self.topLevelItem(i):
577
566
  if tItem.data(self.C_DATA, self.D_HANDLE) == tHandle:
578
567
  tItem.setBackground(self.C_TITLE, self.palette().alternateBase())
579
568
  tItem.setBackground(self.C_WORDS, self.palette().alternateBase())
580
569
  tItem.setBackground(self.C_EXTRA, self.palette().alternateBase())
581
570
  tItem.setBackground(self.C_MORE, self.palette().alternateBase())
571
+ if doScroll and not didScroll:
572
+ self.scrollToItem(tItem, QAbstractItemView.ScrollHint.PositionAtCenter)
573
+ didScroll = True
582
574
  else:
583
575
  tItem.setBackground(self.C_TITLE, self.palette().base())
584
576
  tItem.setBackground(self.C_WORDS, self.palette().base())
585
577
  tItem.setBackground(self.C_EXTRA, self.palette().base())
586
578
  tItem.setBackground(self.C_MORE, self.palette().base())
587
-
588
- logger.debug("Highlighted Novel Tree in %.3f ms", (time() - tStart)*1000)
589
-
590
579
  return
591
580
 
592
581
  ##
@@ -600,12 +589,12 @@ class GuiNovelTree(QTreeWidget):
600
589
  """
601
590
  super().mousePressEvent(event)
602
591
 
603
- if event.button() == Qt.LeftButton:
592
+ if event.button() == Qt.MouseButton.LeftButton:
604
593
  selItem = self.indexAt(event.pos())
605
594
  if not selItem.isValid():
606
595
  self.clearSelection()
607
596
 
608
- elif event.button() == Qt.MiddleButton:
597
+ elif event.button() == Qt.MouseButton.MiddleButton:
609
598
  selItem = self.itemAt(event.pos())
610
599
  if not isinstance(selItem, QTreeWidgetItem):
611
600
  return
@@ -636,7 +625,10 @@ class GuiNovelTree(QTreeWidget):
636
625
  trItem = self.topLevelItem(i)
637
626
  if isinstance(trItem, QTreeWidgetItem):
638
627
  lastText = trItem.data(self.C_DATA, self.D_EXTRA)
639
- trItem.setText(self.C_EXTRA, fMetric.elidedText(lastText, Qt.ElideRight, eliW))
628
+ trItem.setText(
629
+ self.C_EXTRA,
630
+ fMetric.elidedText(lastText, Qt.TextElideMode.ElideRight, eliW)
631
+ )
640
632
  return
641
633
 
642
634
  ##
@@ -683,7 +675,7 @@ class GuiNovelTree(QTreeWidget):
683
675
  tStart = time()
684
676
  logger.debug("Building novel tree for root item '%s'", rootHandle)
685
677
 
686
- novStruct = SHARED.project.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
678
+ novStruct = SHARED.project.index.novelStructure(rootHandle=rootHandle, activeOnly=True)
687
679
  for tKey, tHandle, sTitle, novIdx in novStruct:
688
680
  if novIdx.level == "H0":
689
681
  continue
@@ -692,7 +684,7 @@ class GuiNovelTree(QTreeWidget):
692
684
  newItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
693
685
  newItem.setData(self.C_DATA, self.D_TITLE, sTitle)
694
686
  newItem.setData(self.C_DATA, self.D_KEY, tKey)
695
- newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
687
+ newItem.setTextAlignment(self.C_WORDS, Qt.AlignmentFlag.AlignRight)
696
688
 
697
689
  self._updateTreeItemValues(newItem, novIdx, tHandle, sTitle)
698
690
  self._treeMap[tKey] = newItem
@@ -711,16 +703,16 @@ class GuiNovelTree(QTreeWidget):
711
703
  iLevel = nwHeaders.H_LEVEL.get(idxItem.level, 0)
712
704
  hDec = SHARED.theme.getHeaderDecoration(iLevel)
713
705
 
714
- trItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
706
+ trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec)
715
707
  trItem.setText(self.C_TITLE, idxItem.title)
716
708
  trItem.setFont(self.C_TITLE, self._hFonts[iLevel])
717
709
  trItem.setText(self.C_WORDS, f"{idxItem.wordCount:n}")
718
- trItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore)
710
+ trItem.setData(self.C_MORE, Qt.ItemDataRole.DecorationRole, self._pMore)
719
711
 
720
712
  # Custom column
721
713
  mW = int(self._lastColSize * self.viewport().width())
722
714
  lastText, toolTip = self._getLastColumnText(tHandle, sTitle)
723
- elideText = self.fontMetrics().elidedText(lastText, Qt.ElideRight, mW)
715
+ elideText = self.fontMetrics().elidedText(lastText, Qt.TextElideMode.ElideRight, mW)
724
716
  trItem.setText(self.C_EXTRA, elideText)
725
717
  trItem.setData(self.C_DATA, self.D_EXTRA, lastText)
726
718
  trItem.setToolTip(self.C_EXTRA, toolTip)
@@ -734,17 +726,17 @@ class GuiNovelTree(QTreeWidget):
734
726
 
735
727
  refData = []
736
728
  refName = ""
737
- theRefs = SHARED.project.index.getReferences(tHandle, sTitle)
729
+ refs = SHARED.project.index.getReferences(tHandle, sTitle)
738
730
  if self._lastCol == NovelTreeColumn.POV:
739
- refData = theRefs[nwKeyWords.POV_KEY]
731
+ refData = refs[nwKeyWords.POV_KEY]
740
732
  refName = self._povLabel
741
733
 
742
734
  elif self._lastCol == NovelTreeColumn.FOCUS:
743
- refData = theRefs[nwKeyWords.FOCUS_KEY]
735
+ refData = refs[nwKeyWords.FOCUS_KEY]
744
736
  refName = self._focLabel
745
737
 
746
738
  elif self._lastCol == NovelTreeColumn.PLOT:
747
- refData = theRefs[nwKeyWords.PLOT_KEY]
739
+ refData = refs[nwKeyWords.PLOT_KEY]
748
740
  refName = self._pltLabel
749
741
 
750
742
  if refData:
@@ -10,7 +10,7 @@ Created: 2019-11-16 [0.4.1] GuiOutlineHeaderMenu
10
10
  Created: 2020-06-02 [0.7] GuiOutlineDetails
11
11
 
12
12
  This file is a part of novelWriter
13
- Copyright 2018–2023, Veronica Berglyd Olsen
13
+ Copyright 2018–2024, Veronica Berglyd Olsen
14
14
 
15
15
  This program is free software: you can redistribute it and/or modify
16
16
  it under the terms of the GNU General Public License as published by
@@ -27,18 +27,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
27
27
  """
28
28
  from __future__ import annotations
29
29
 
30
+ import csv
30
31
  import logging
31
32
 
32
33
  from time import time
33
34
  from enum import Enum
34
35
 
35
- from PyQt5.QtCore import (
36
- Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP
37
- )
36
+ from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP
38
37
  from PyQt5.QtWidgets import (
39
- QAbstractItemView, QAction, QFrame, QGridLayout, QGroupBox, QHBoxLayout,
40
- QLabel, QMenu, QScrollArea, QSizePolicy, QSplitter, QToolBar, QToolButton,
41
- QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
38
+ QAbstractItemView, QAction, QFileDialog, QFrame, QGridLayout, QGroupBox,
39
+ QHBoxLayout, QLabel, QMenu, QScrollArea, QSizePolicy, QSplitter, QToolBar,
40
+ QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
42
41
  )
43
42
 
44
43
  from novelwriter import CONFIG, SHARED
@@ -46,7 +45,7 @@ from novelwriter.enum import (
46
45
  nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline
47
46
  )
48
47
  from novelwriter.error import logException
49
- from novelwriter.common import checkInt
48
+ from novelwriter.common import checkInt, formatFileFilter, makeFileNameSafe
50
49
  from novelwriter.constants import nwHeaders, trConst, nwKeyWords, nwLabels
51
50
  from novelwriter.extensions.novelselector import NovelSelector
52
51
 
@@ -88,6 +87,7 @@ class GuiOutlineView(QWidget):
88
87
  self.outlineData.itemTagClicked.connect(self._tagClicked)
89
88
  self.outlineBar.loadNovelRootRequest.connect(self._rootItemChanged)
90
89
  self.outlineBar.viewColumnToggled.connect(self.outlineTree.menuColumnToggled)
90
+ self.outlineBar.outlineExportRequest.connect(self.outlineTree.exportOutline)
91
91
 
92
92
  # Function Mappings
93
93
  self.getSelectedHandle = self.outlineTree.getSelectedHandle
@@ -124,7 +124,7 @@ class GuiOutlineView(QWidget):
124
124
  def openProjectTasks(self) -> None:
125
125
  """Run open project tasks."""
126
126
  lastOutline = SHARED.project.data.getLastHandle("outline")
127
- if not (lastOutline is None or lastOutline in SHARED.project.tree):
127
+ if not lastOutline or lastOutline not in SHARED.project.tree:
128
128
  lastOutline = SHARED.project.tree.findRoot(nwItemClass.NOVEL)
129
129
 
130
130
  logger.debug("Setting outline tree to root item '%s'", lastOutline)
@@ -198,6 +198,7 @@ class GuiOutlineView(QWidget):
198
198
  class GuiOutlineToolBar(QToolBar):
199
199
 
200
200
  loadNovelRootRequest = pyqtSignal(str)
201
+ outlineExportRequest = pyqtSignal()
201
202
  viewColumnToggled = pyqtSignal(bool, Enum)
202
203
 
203
204
  def __init__(self, outlineView: GuiOutlineView) -> None:
@@ -220,6 +221,7 @@ class GuiOutlineToolBar(QToolBar):
220
221
  self.novelLabel.setContentsMargins(0, 0, mPx, 0)
221
222
 
222
223
  self.novelValue = NovelSelector(self)
224
+ self.novelValue.setIncludeAll(True)
223
225
  self.novelValue.setMinimumWidth(CONFIG.pxInt(200))
224
226
  self.novelValue.novelSelectionChanged.connect(self._novelValueChanged)
225
227
 
@@ -227,6 +229,9 @@ class GuiOutlineToolBar(QToolBar):
227
229
  self.aRefresh = QAction(self.tr("Refresh"), self)
228
230
  self.aRefresh.triggered.connect(self._refreshRequested)
229
231
 
232
+ self.aExport = QAction(self.tr("Export CSV"), self)
233
+ self.aExport.triggered.connect(self._exportRequested)
234
+
230
235
  # Column Menu
231
236
  self.mColumns = GuiOutlineHeaderMenu(self)
232
237
  self.mColumns.columnToggled.connect(
@@ -242,6 +247,7 @@ class GuiOutlineToolBar(QToolBar):
242
247
  self.addWidget(self.novelValue)
243
248
  self.addSeparator()
244
249
  self.addAction(self.aRefresh)
250
+ self.addAction(self.aExport)
245
251
  self.addWidget(self.tbColumns)
246
252
  self.addWidget(stretch)
247
253
 
@@ -258,15 +264,16 @@ class GuiOutlineToolBar(QToolBar):
258
264
  def updateTheme(self) -> None:
259
265
  """Update theme elements."""
260
266
  self.setStyleSheet("QToolBar {border: 0px;}")
261
- self.novelValue.updateList(includeAll=True)
267
+ self.novelValue.refreshNovelList()
262
268
  self.aRefresh.setIcon(SHARED.theme.getIcon("refresh"))
269
+ self.aExport.setIcon(SHARED.theme.getIcon("export"))
263
270
  self.tbColumns.setIcon(SHARED.theme.getIcon("menu"))
264
271
  self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}")
265
272
  return
266
273
 
267
274
  def populateNovelList(self) -> None:
268
275
  """Reload the content of the novel list."""
269
- self.novelValue.updateList(includeAll=True)
276
+ self.novelValue.refreshNovelList()
270
277
  return
271
278
 
272
279
  def setCurrentRoot(self, rootHandle: str | None) -> None:
@@ -295,6 +302,12 @@ class GuiOutlineToolBar(QToolBar):
295
302
  self.loadNovelRootRequest.emit(self.novelValue.handle)
296
303
  return
297
304
 
305
+ @pyqtSlot()
306
+ def _exportRequested(self) -> None:
307
+ """Emit a signal that an export of the outline was requested."""
308
+ self.outlineExportRequest.emit()
309
+ return
310
+
298
311
  # END Class GuiOutlineToolBar
299
312
 
300
313
 
@@ -491,15 +504,48 @@ class GuiOutlineTree(QTreeWidget):
491
504
  """Get the currently selected handle. If multiple items are
492
505
  selected, return the first.
493
506
  """
494
- selItem = self.selectedItems()
495
- if selItem:
496
- tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
497
- sTitle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
507
+ if item := self.selectedItems():
508
+ tHandle = item[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
509
+ sTitle = item[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
498
510
  return tHandle, sTitle
499
511
  return None, None
500
512
 
501
513
  ##
502
- # Slots
514
+ # Public Slots
515
+ ##
516
+
517
+ @pyqtSlot(bool, Enum)
518
+ def menuColumnToggled(self, isChecked: bool, hItem: nwOutline) -> None:
519
+ """Receive the changes to column visibility forwarded by the
520
+ column selection menu.
521
+ """
522
+ if hItem in self._colIdx:
523
+ self.setColumnHidden(self._colIdx[hItem], not isChecked)
524
+ self._saveHeaderState()
525
+ return
526
+
527
+ @pyqtSlot()
528
+ def exportOutline(self) -> None:
529
+ """Export the outline as a CSV file."""
530
+ path = CONFIG.lastPath() / f"{makeFileNameSafe(SHARED.project.data.name)}.csv"
531
+ path, _ = QFileDialog.getSaveFileName(
532
+ self, self.tr("Save Outline As"), str(path), formatFileFilter(["*.csv", "*"])
533
+ )
534
+ if path:
535
+ CONFIG.setLastPath(path)
536
+ logger.info("Writing CSV file: %s", path)
537
+ cols = [col for col in self._treeOrder if not self._colHidden[col]]
538
+ order = [self._colIdx[col] for col in cols]
539
+ with open(path, mode="w", newline="") as csvFile:
540
+ writer = csv.writer(csvFile, dialect="excel", quoting=csv.QUOTE_ALL)
541
+ writer.writerow([trConst(nwLabels.OUTLINE_COLS[col]) for col in cols])
542
+ for i in range(self.topLevelItemCount()):
543
+ if item := self.topLevelItem(i):
544
+ writer.writerow(item.text(i) for i in order)
545
+ return
546
+
547
+ ##
548
+ # Private Slots
503
549
  ##
504
550
 
505
551
  @pyqtSlot("QTreeWidgetItem*", int)
@@ -509,9 +555,8 @@ class GuiOutlineTree(QTreeWidget):
509
555
  document editor.
510
556
  """
511
557
  tHandle, sTitle = self.getSelectedHandle()
512
- if tHandle is None:
513
- return
514
- self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True)
558
+ if tHandle:
559
+ self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True)
515
560
  return
516
561
 
517
562
  @pyqtSlot()
@@ -535,16 +580,6 @@ class GuiOutlineTree(QTreeWidget):
535
580
  self._saveHeaderState()
536
581
  return
537
582
 
538
- @pyqtSlot(bool, Enum)
539
- def menuColumnToggled(self, isChecked: bool, hItem: nwOutline) -> None:
540
- """Receive the changes to column visibility forwarded by the
541
- column selection menu.
542
- """
543
- if hItem in self._colIdx:
544
- self.setColumnHidden(self._colIdx[hItem], not isChecked)
545
- self._saveHeaderState()
546
- return
547
-
548
583
  ##
549
584
  # Internal Functions
550
585
  ##
@@ -606,6 +641,7 @@ class GuiOutlineTree(QTreeWidget):
606
641
  logHidden, orgWidth if logHidden and logWidth == 0 else logWidth
607
642
  ]
608
643
 
644
+ logger.debug("Saving State: GuiOutline")
609
645
  pOptions = SHARED.project.options
610
646
  pOptions.setValue("GuiOutline", "columnState", colState)
611
647
  pOptions.saveSettings()
@@ -623,12 +659,12 @@ class GuiOutlineTree(QTreeWidget):
623
659
  self.clear()
624
660
 
625
661
  if self._firstView:
626
- theLabels = []
662
+ labels = []
627
663
  for i, hItem in enumerate(self._treeOrder):
628
- theLabels.append(trConst(nwLabels.OUTLINE_COLS[hItem]))
664
+ labels.append(trConst(nwLabels.OUTLINE_COLS[hItem]))
629
665
  self._colIdx[hItem] = i
630
666
 
631
- self.setHeaderLabels(theLabels)
667
+ self.setHeaderLabels(labels)
632
668
  for hItem in self._treeOrder:
633
669
  self.setColumnWidth(self._colIdx[hItem], self._colWidth[hItem])
634
670
  self.setColumnHidden(self._colIdx[hItem], self._colHidden[hItem])
@@ -645,7 +681,7 @@ class GuiOutlineTree(QTreeWidget):
645
681
  headItem.setTextAlignment(
646
682
  self._colIdx[nwOutline.PCOUNT], Qt.AlignmentFlag.AlignRight)
647
683
 
648
- novStruct = SHARED.project.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
684
+ novStruct = SHARED.project.index.novelStructure(rootHandle=rootHandle, activeOnly=True)
649
685
  for _, tHandle, sTitle, novIdx in novStruct:
650
686
 
651
687
  iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0)
@@ -841,18 +877,15 @@ class GuiOutlineDetails(QScrollArea):
841
877
  self.entKeyValue.setWordWrap(True)
842
878
  self.cstKeyValue.setWordWrap(True)
843
879
 
844
- def tagClicked(link):
845
- self.itemTagClicked.emit(link)
846
-
847
- self.povKeyValue.linkActivated.connect(tagClicked)
848
- self.focKeyValue.linkActivated.connect(tagClicked)
849
- self.chrKeyValue.linkActivated.connect(tagClicked)
850
- self.pltKeyValue.linkActivated.connect(tagClicked)
851
- self.timKeyValue.linkActivated.connect(tagClicked)
852
- self.wldKeyValue.linkActivated.connect(tagClicked)
853
- self.objKeyValue.linkActivated.connect(tagClicked)
854
- self.entKeyValue.linkActivated.connect(tagClicked)
855
- self.cstKeyValue.linkActivated.connect(tagClicked)
880
+ self.povKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x))
881
+ self.focKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x))
882
+ self.chrKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x))
883
+ self.pltKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x))
884
+ self.timKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x))
885
+ self.wldKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x))
886
+ self.objKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x))
887
+ self.entKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x))
888
+ self.cstKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x))
856
889
 
857
890
  self.povKeyLWrap.addWidget(self.povKeyValue, 1)
858
891
  self.focKeyLWrap.addWidget(self.focKeyValue, 1)
@@ -981,49 +1014,40 @@ class GuiOutlineDetails(QScrollArea):
981
1014
  ##
982
1015
 
983
1016
  @pyqtSlot(str, str)
984
- def showItem(self, tHandle: str, sTitle: str) -> bool:
1017
+ def showItem(self, tHandle: str, sTitle: str) -> None:
985
1018
  """Update the content of the tree with the given handle and line
986
1019
  number pointing to a header.
987
1020
  """
988
1021
  pIndex = SHARED.project.index
989
1022
  nwItem = SHARED.project.tree[tHandle]
990
1023
  novIdx = pIndex.getItemHeader(tHandle, sTitle)
991
- theRefs = pIndex.getReferences(tHandle, sTitle)
992
- if nwItem is None or novIdx is None:
993
- return False
994
-
995
- if novIdx.level in self.LVL_MAP:
996
- self.titleLabel.setText("<b>%s</b>" % self.tr(self.LVL_MAP[novIdx.level]))
997
- else:
998
- self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
999
- self.titleValue.setText(novIdx.title)
1024
+ novRefs = pIndex.getReferences(tHandle, sTitle)
1025
+ if nwItem and novIdx:
1026
+ self.titleLabel.setText("<b>%s</b>" % self.tr(self.LVL_MAP.get(novIdx.level, "H1")))
1027
+ self.titleValue.setText(novIdx.title)
1000
1028
 
1001
- itemStatus, _ = nwItem.getImportStatus(incIcon=False)
1029
+ itemStatus, _ = nwItem.getImportStatus(incIcon=False)
1002
1030
 
1003
- self.fileValue.setText(nwItem.itemName)
1004
- self.itemValue.setText(itemStatus)
1031
+ self.fileValue.setText(nwItem.itemName)
1032
+ self.itemValue.setText(itemStatus)
1005
1033
 
1006
- cC = checkInt(novIdx.charCount, 0)
1007
- wC = checkInt(novIdx.wordCount, 0)
1008
- pC = checkInt(novIdx.paraCount, 0)
1034
+ self.cCValue.setText(f"{checkInt(novIdx.charCount, 0):n}")
1035
+ self.wCValue.setText(f"{checkInt(novIdx.wordCount, 0):n}")
1036
+ self.pCValue.setText(f"{checkInt(novIdx.paraCount, 0):n}")
1009
1037
 
1010
- self.cCValue.setText(f"{cC:n}")
1011
- self.wCValue.setText(f"{wC:n}")
1012
- self.pCValue.setText(f"{pC:n}")
1038
+ self.synopValue.setText(novIdx.synopsis)
1013
1039
 
1014
- self.synopValue.setText(novIdx.synopsis)
1040
+ self.povKeyValue.setText(self._formatTags(novRefs, nwKeyWords.POV_KEY))
1041
+ self.focKeyValue.setText(self._formatTags(novRefs, nwKeyWords.FOCUS_KEY))
1042
+ self.chrKeyValue.setText(self._formatTags(novRefs, nwKeyWords.CHAR_KEY))
1043
+ self.pltKeyValue.setText(self._formatTags(novRefs, nwKeyWords.PLOT_KEY))
1044
+ self.timKeyValue.setText(self._formatTags(novRefs, nwKeyWords.TIME_KEY))
1045
+ self.wldKeyValue.setText(self._formatTags(novRefs, nwKeyWords.WORLD_KEY))
1046
+ self.objKeyValue.setText(self._formatTags(novRefs, nwKeyWords.OBJECT_KEY))
1047
+ self.entKeyValue.setText(self._formatTags(novRefs, nwKeyWords.ENTITY_KEY))
1048
+ self.cstKeyValue.setText(self._formatTags(novRefs, nwKeyWords.CUSTOM_KEY))
1015
1049
 
1016
- self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY))
1017
- self.focKeyValue.setText(self._formatTags(theRefs, nwKeyWords.FOCUS_KEY))
1018
- self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY))
1019
- self.pltKeyValue.setText(self._formatTags(theRefs, nwKeyWords.PLOT_KEY))
1020
- self.timKeyValue.setText(self._formatTags(theRefs, nwKeyWords.TIME_KEY))
1021
- self.wldKeyValue.setText(self._formatTags(theRefs, nwKeyWords.WORLD_KEY))
1022
- self.objKeyValue.setText(self._formatTags(theRefs, nwKeyWords.OBJECT_KEY))
1023
- self.entKeyValue.setText(self._formatTags(theRefs, nwKeyWords.ENTITY_KEY))
1024
- self.cstKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CUSTOM_KEY))
1025
-
1026
- return True
1050
+ return
1027
1051
 
1028
1052
  @pyqtSlot()
1029
1053
  def updateClasses(self) -> None: