novelWriter 2.2.1__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 (110) hide show
  1. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/RECORD +102 -92
  3. novelwriter/__init__.py +4 -4
  4. novelwriter/assets/icons/typicons_dark/icons.conf +6 -0
  5. novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
  6. novelwriter/assets/icons/typicons_dark/typ_document-add-col.svg +8 -0
  7. novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
  8. novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
  9. novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
  10. novelwriter/assets/icons/typicons_light/icons.conf +6 -0
  11. novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
  12. novelwriter/assets/icons/typicons_light/typ_document-add-col.svg +8 -0
  13. novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
  14. novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
  15. novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
  16. novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
  17. novelwriter/assets/images/novelwriter-text-light.svg +4 -0
  18. novelwriter/assets/images/welcome-dark.jpg +0 -0
  19. novelwriter/assets/images/welcome-light.jpg +0 -0
  20. novelwriter/assets/manual.pdf +0 -0
  21. novelwriter/assets/sample.zip +0 -0
  22. novelwriter/assets/syntax/default_dark.conf +1 -0
  23. novelwriter/assets/syntax/default_light.conf +1 -0
  24. novelwriter/assets/syntax/grey_dark.conf +1 -0
  25. novelwriter/assets/syntax/grey_light.conf +1 -0
  26. novelwriter/assets/syntax/light_owl.conf +1 -0
  27. novelwriter/assets/syntax/night_owl.conf +1 -0
  28. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  29. novelwriter/assets/syntax/solarized_light.conf +1 -0
  30. novelwriter/assets/syntax/tomorrow.conf +1 -0
  31. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  32. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  33. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  34. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  35. novelwriter/assets/text/credits_en.htm +4 -2
  36. novelwriter/assets/themes/default_dark.conf +2 -2
  37. novelwriter/assets/themes/default_light.conf +2 -2
  38. novelwriter/common.py +48 -37
  39. novelwriter/config.py +36 -41
  40. novelwriter/constants.py +38 -16
  41. novelwriter/core/buildsettings.py +7 -7
  42. novelwriter/core/coretools.py +192 -154
  43. novelwriter/core/docbuild.py +6 -3
  44. novelwriter/core/document.py +6 -6
  45. novelwriter/core/index.py +89 -56
  46. novelwriter/core/item.py +21 -3
  47. novelwriter/core/options.py +8 -7
  48. novelwriter/core/project.py +69 -44
  49. novelwriter/core/projectdata.py +1 -14
  50. novelwriter/core/projectxml.py +13 -41
  51. novelwriter/core/sessions.py +2 -1
  52. novelwriter/core/spellcheck.py +2 -1
  53. novelwriter/core/status.py +2 -1
  54. novelwriter/core/storage.py +178 -140
  55. novelwriter/core/tohtml.py +4 -2
  56. novelwriter/core/tokenizer.py +73 -45
  57. novelwriter/core/toodt.py +40 -30
  58. novelwriter/core/tree.py +3 -2
  59. novelwriter/dialogs/about.py +70 -160
  60. novelwriter/dialogs/docmerge.py +6 -5
  61. novelwriter/dialogs/docsplit.py +6 -6
  62. novelwriter/dialogs/editlabel.py +1 -1
  63. novelwriter/dialogs/preferences.py +553 -703
  64. novelwriter/dialogs/{projsettings.py → projectsettings.py} +288 -262
  65. novelwriter/dialogs/quotes.py +27 -23
  66. novelwriter/dialogs/wordlist.py +96 -40
  67. novelwriter/enum.py +20 -18
  68. novelwriter/error.py +1 -1
  69. novelwriter/extensions/circularprogress.py +11 -11
  70. novelwriter/extensions/configlayout.py +185 -134
  71. novelwriter/extensions/modified.py +81 -0
  72. novelwriter/extensions/novelselector.py +26 -12
  73. novelwriter/extensions/pagedsidebar.py +14 -16
  74. novelwriter/extensions/simpleprogress.py +5 -5
  75. novelwriter/extensions/statusled.py +8 -8
  76. novelwriter/extensions/switch.py +31 -63
  77. novelwriter/extensions/switchbox.py +1 -1
  78. novelwriter/extensions/versioninfo.py +153 -0
  79. novelwriter/gui/doceditor.py +178 -150
  80. novelwriter/gui/dochighlight.py +63 -92
  81. novelwriter/gui/docviewer.py +49 -51
  82. novelwriter/gui/docviewerpanel.py +72 -24
  83. novelwriter/gui/itemdetails.py +7 -7
  84. novelwriter/gui/mainmenu.py +14 -18
  85. novelwriter/gui/noveltree.py +9 -8
  86. novelwriter/gui/outline.py +98 -75
  87. novelwriter/gui/projtree.py +188 -61
  88. novelwriter/gui/sidebar.py +3 -4
  89. novelwriter/gui/statusbar.py +3 -4
  90. novelwriter/gui/theme.py +60 -68
  91. novelwriter/guimain.py +49 -156
  92. novelwriter/shared.py +15 -1
  93. novelwriter/tools/dictionaries.py +5 -6
  94. novelwriter/tools/manuscript.py +6 -6
  95. novelwriter/tools/manussettings.py +192 -221
  96. novelwriter/tools/noveldetails.py +525 -0
  97. novelwriter/tools/welcome.py +802 -0
  98. novelwriter/tools/writingstats.py +9 -9
  99. novelwriter/assets/images/wizard-back.jpg +0 -0
  100. novelwriter/assets/text/gplv3_en.htm +0 -641
  101. novelwriter/assets/text/release_notes.htm +0 -60
  102. novelwriter/dialogs/projdetails.py +0 -518
  103. novelwriter/dialogs/projload.py +0 -294
  104. novelwriter/dialogs/updates.py +0 -172
  105. novelwriter/extensions/pageddialog.py +0 -130
  106. novelwriter/tools/projwizard.py +0 -478
  107. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/LICENSE.md +0 -0
  108. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/WHEEL +0 -0
  109. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/entry_points.txt +0 -0
  110. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/top_level.txt +0 -0
@@ -56,7 +56,6 @@ from novelwriter import CONFIG, SHARED
56
56
  from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
57
57
  from novelwriter.common import minmax, transferCase
58
58
  from novelwriter.constants import nwKeyWords, nwLabels, nwShortcode, nwUnicode, trConst
59
- from novelwriter.core.item import NWItem
60
59
  from novelwriter.core.index import countWords
61
60
  from novelwriter.tools.lipsum import GuiLipsum
62
61
  from novelwriter.core.document import NWDocument
@@ -187,13 +186,12 @@ class GuiDocEditor(QPlainTextEdit):
187
186
  # Set Up Document Word Counter
188
187
  self.wcTimerDoc = QTimer()
189
188
  self.wcTimerDoc.timeout.connect(self._runDocCounter)
189
+ self.wcTimerDoc.setInterval(5000)
190
190
 
191
191
  self.wCounterDoc = BackgroundWordCounter(self)
192
192
  self.wCounterDoc.setAutoDelete(False)
193
193
  self.wCounterDoc.signals.countsReady.connect(self._updateDocCounts)
194
194
 
195
- self.wcInterval = CONFIG.wordCountTimer
196
-
197
195
  # Set Up Selection Word Counter
198
196
  self.wcTimerSel = QTimer()
199
197
  self.wcTimerSel.timeout.connect(self._runSelCounter)
@@ -277,14 +275,14 @@ class GuiDocEditor(QPlainTextEdit):
277
275
  def updateSyntaxColours(self) -> None:
278
276
  """Update the syntax highlighting theme."""
279
277
  mainPalette = self.palette()
280
- mainPalette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
281
- mainPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack))
282
- mainPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
278
+ mainPalette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
279
+ mainPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
280
+ mainPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
283
281
  self.setPalette(mainPalette)
284
282
 
285
283
  docPalette = self.viewport().palette()
286
- docPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack))
287
- docPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
284
+ docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
285
+ docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
288
286
  self.viewport().setPalette(docPalette)
289
287
 
290
288
  self.docHeader.matchColours()
@@ -320,10 +318,10 @@ class GuiDocEditor(QPlainTextEdit):
320
318
  SHARED.updateSpellCheckLanguage()
321
319
 
322
320
  # Set font
323
- textFont = QFont()
324
- textFont.setFamily(CONFIG.textFont)
325
- textFont.setPointSize(CONFIG.textSize)
326
- self.setFont(textFont)
321
+ font = QFont()
322
+ font.setFamily(CONFIG.textFont)
323
+ font.setPointSize(CONFIG.textSize)
324
+ self.setFont(font)
327
325
 
328
326
  # Set default text margins
329
327
  # Due to cursor visibility, a part of the margin must be
@@ -359,17 +357,14 @@ class GuiDocEditor(QPlainTextEdit):
359
357
  # Refresh the tab stops
360
358
  self.setTabStopDistance(CONFIG.getTabWidth())
361
359
 
362
- # Configure word count timer
363
- self.wcInterval = CONFIG.wordCountTimer
364
- self.wcTimerDoc.setInterval(int(self.wcInterval*1000))
365
-
366
- # If we have a document open, we should reload it in case the
360
+ # If we have a document open, we should refresh it in case the
367
361
  # font changed, otherwise we just clear the editor entirely,
368
362
  # which makes it read only.
369
- if self._docHandle is None:
370
- self.clearEditor()
371
- else:
363
+ if self._docHandle:
372
364
  self._qDocument.syntaxHighlighter.rehighlight()
365
+ self.docHeader.setTitleFromHandle(self._docHandle)
366
+ else:
367
+ self.clearEditor()
373
368
 
374
369
  return
375
370
 
@@ -741,9 +736,11 @@ class GuiDocEditor(QPlainTextEdit):
741
736
  elif action == nwDocAction.BLOCK_H4:
742
737
  self._formatBlock(nwDocAction.BLOCK_H4)
743
738
  elif action == nwDocAction.BLOCK_COM:
744
- self._formatBlock(nwDocAction.BLOCK_COM)
739
+ self._iterFormatBlocks(nwDocAction.BLOCK_COM)
740
+ elif action == nwDocAction.BLOCK_IGN:
741
+ self._iterFormatBlocks(nwDocAction.BLOCK_IGN)
745
742
  elif action == nwDocAction.BLOCK_TXT:
746
- self._formatBlock(nwDocAction.BLOCK_TXT)
743
+ self._iterFormatBlocks(nwDocAction.BLOCK_TXT)
747
744
  elif action == nwDocAction.BLOCK_TTL:
748
745
  self._formatBlock(nwDocAction.BLOCK_TTL)
749
746
  elif action == nwDocAction.BLOCK_UNN:
@@ -1190,7 +1187,7 @@ class GuiDocEditor(QPlainTextEdit):
1190
1187
  logger.debug("Word counter is busy")
1191
1188
  return
1192
1189
 
1193
- if time() - self._lastEdit < 5.0 * self.wcInterval:
1190
+ if time() - self._lastEdit < 25.0:
1194
1191
  logger.debug("Running word counter")
1195
1192
  SHARED.runInThreadPool(self.wCounterDoc)
1196
1193
 
@@ -1593,9 +1590,7 @@ class GuiDocEditor(QPlainTextEdit):
1593
1590
 
1594
1591
  posS = cursor.selectionStart()
1595
1592
  posE = cursor.selectionEnd()
1596
- closeCheck = (
1597
- " ", "\n", nwUnicode.U_LSEP, nwUnicode.U_PSEP
1598
- )
1593
+ closeCheck = (" ", "\n", nwUnicode.U_LSEP, nwUnicode.U_PSEP)
1599
1594
 
1600
1595
  self._allowAutoReplace(False)
1601
1596
  for posC in range(posS, posE+1):
@@ -1639,159 +1634,189 @@ class GuiDocEditor(QPlainTextEdit):
1639
1634
 
1640
1635
  return True
1641
1636
 
1642
- def _formatBlock(self, action: nwDocAction) -> bool:
1643
- """Change the block format of the block under the cursor."""
1644
- cursor = self.textCursor()
1645
- block = cursor.block()
1646
- if not block.isValid():
1647
- logger.debug("Invalid block selected for action '%s'", str(action))
1648
- return False
1649
-
1637
+ def _processBlockFormat(
1638
+ self, action: nwDocAction, text: str, toggle: bool = True
1639
+ ) -> tuple[nwDocAction, str, int]:
1640
+ """Process the formatting of a single text block."""
1650
1641
  # Remove existing format first, if any
1651
- setText = block.text()
1652
- hasText = len(setText) > 0
1653
- if setText.startswith("@"):
1642
+ if text.startswith("@"):
1654
1643
  logger.error("Cannot apply block format to keyword/value line")
1655
- return False
1656
- elif setText.startswith("% "):
1657
- newText = setText[2:]
1658
- cOffset = 2
1659
- if action == nwDocAction.BLOCK_COM:
1644
+ return nwDocAction.NO_ACTION, "", 0
1645
+ elif text.startswith("%~"):
1646
+ temp = text[2:].lstrip()
1647
+ offset = len(text) - len(temp)
1648
+ if toggle and action == nwDocAction.BLOCK_IGN:
1660
1649
  action = nwDocAction.BLOCK_TXT
1661
- elif setText.startswith("%"):
1662
- newText = setText[1:]
1663
- cOffset = 1
1664
- if action == nwDocAction.BLOCK_COM:
1650
+ elif text.startswith("%"):
1651
+ temp = text[1:].lstrip()
1652
+ offset = len(text) - len(temp)
1653
+ if toggle and action == nwDocAction.BLOCK_COM:
1665
1654
  action = nwDocAction.BLOCK_TXT
1666
- elif setText.startswith("# "):
1667
- newText = setText[2:]
1668
- cOffset = 2
1669
- elif setText.startswith("## "):
1670
- newText = setText[3:]
1671
- cOffset = 3
1672
- elif setText.startswith("### "):
1673
- newText = setText[4:]
1674
- cOffset = 4
1675
- elif setText.startswith("#### "):
1676
- newText = setText[5:]
1677
- cOffset = 5
1678
- elif setText.startswith("#! "):
1679
- newText = setText[3:]
1680
- cOffset = 3
1681
- elif setText.startswith("##! "):
1682
- newText = setText[4:]
1683
- cOffset = 4
1684
- elif setText.startswith(">> "):
1685
- newText = setText[3:]
1686
- cOffset = 3
1687
- elif setText.startswith("> ") and action != nwDocAction.INDENT_R:
1688
- newText = setText[2:]
1689
- cOffset = 2
1690
- elif setText.startswith(">>"):
1691
- newText = setText[2:]
1692
- cOffset = 2
1693
- elif setText.startswith(">") and action != nwDocAction.INDENT_R:
1694
- newText = setText[1:]
1695
- cOffset = 1
1655
+ elif text.startswith("# "):
1656
+ temp = text[2:]
1657
+ offset = 2
1658
+ elif text.startswith("## "):
1659
+ temp = text[3:]
1660
+ offset = 3
1661
+ elif text.startswith("### "):
1662
+ temp = text[4:]
1663
+ offset = 4
1664
+ elif text.startswith("#### "):
1665
+ temp = text[5:]
1666
+ offset = 5
1667
+ elif text.startswith("#! "):
1668
+ temp = text[3:]
1669
+ offset = 3
1670
+ elif text.startswith("##! "):
1671
+ temp = text[4:]
1672
+ offset = 4
1673
+ elif text.startswith(">> "):
1674
+ temp = text[3:]
1675
+ offset = 3
1676
+ elif text.startswith("> ") and action != nwDocAction.INDENT_R:
1677
+ temp = text[2:]
1678
+ offset = 2
1679
+ elif text.startswith(">>"):
1680
+ temp = text[2:]
1681
+ offset = 2
1682
+ elif text.startswith(">") and action != nwDocAction.INDENT_R:
1683
+ temp = text[1:]
1684
+ offset = 1
1696
1685
  else:
1697
- newText = setText
1698
- cOffset = 0
1686
+ temp = text
1687
+ offset = 0
1699
1688
 
1700
1689
  # Also remove formatting tags at the end
1701
- if setText.endswith(" <<"):
1702
- newText = newText[:-3]
1703
- elif setText.endswith(" <") and action != nwDocAction.INDENT_L:
1704
- newText = newText[:-2]
1705
- elif setText.endswith("<<"):
1706
- newText = newText[:-2]
1707
- elif setText.endswith("<") and action != nwDocAction.INDENT_L:
1708
- newText = newText[:-1]
1690
+ if text.endswith(" <<"):
1691
+ temp = temp[:-3]
1692
+ elif text.endswith(" <") and action != nwDocAction.INDENT_L:
1693
+ temp = temp[:-2]
1694
+ elif text.endswith("<<"):
1695
+ temp = temp[:-2]
1696
+ elif text.endswith("<") and action != nwDocAction.INDENT_L:
1697
+ temp = temp[:-1]
1709
1698
 
1710
1699
  # Apply new format
1711
1700
  if action == nwDocAction.BLOCK_COM:
1712
- setText = "% "+newText
1713
- cOffset -= 2
1701
+ text = f"% {temp}"
1702
+ offset -= 2
1703
+ elif action == nwDocAction.BLOCK_IGN:
1704
+ text = f"%~ {temp}"
1705
+ offset -= 3
1714
1706
  elif action == nwDocAction.BLOCK_H1:
1715
- setText = "# "+newText
1716
- cOffset -= 2
1707
+ text = f"# {temp}"
1708
+ offset -= 2
1717
1709
  elif action == nwDocAction.BLOCK_H2:
1718
- setText = "## "+newText
1719
- cOffset -= 3
1710
+ text = f"## {temp}"
1711
+ offset -= 3
1720
1712
  elif action == nwDocAction.BLOCK_H3:
1721
- setText = "### "+newText
1722
- cOffset -= 4
1713
+ text = f"### {temp}"
1714
+ offset -= 4
1723
1715
  elif action == nwDocAction.BLOCK_H4:
1724
- setText = "#### "+newText
1725
- cOffset -= 5
1716
+ text = f"#### {temp}"
1717
+ offset -= 5
1726
1718
  elif action == nwDocAction.BLOCK_TTL:
1727
- setText = "#! "+newText
1728
- cOffset -= 3
1719
+ text = f"#! {temp}"
1720
+ offset -= 3
1729
1721
  elif action == nwDocAction.BLOCK_UNN:
1730
- setText = "##! "+newText
1731
- cOffset -= 4
1722
+ text = f"##! {temp}"
1723
+ offset -= 4
1732
1724
  elif action == nwDocAction.ALIGN_L:
1733
- setText = newText+" <<"
1725
+ text = f"{temp} <<"
1734
1726
  elif action == nwDocAction.ALIGN_C:
1735
- setText = ">> "+newText+" <<"
1736
- cOffset -= 3
1727
+ text = f">> {temp} <<"
1728
+ offset -= 3
1737
1729
  elif action == nwDocAction.ALIGN_R:
1738
- setText = ">> "+newText
1739
- cOffset -= 3
1730
+ text = f">> {temp}"
1731
+ offset -= 3
1740
1732
  elif action == nwDocAction.INDENT_L:
1741
- setText = "> "+newText
1742
- cOffset -= 2
1733
+ text = f"> {temp}"
1734
+ offset -= 2
1743
1735
  elif action == nwDocAction.INDENT_R:
1744
- setText = newText+" <"
1736
+ text = f"{temp} <"
1745
1737
  elif action == nwDocAction.BLOCK_TXT:
1746
- setText = newText
1738
+ text = temp
1747
1739
  else:
1748
1740
  logger.error("Unknown or unsupported block format requested: '%s'", str(action))
1741
+ return nwDocAction.NO_ACTION, "", 0
1742
+
1743
+ return action, text, offset
1744
+
1745
+ def _formatBlock(self, action: nwDocAction) -> bool:
1746
+ """Change the block format of the block under the cursor."""
1747
+ cursor = self.textCursor()
1748
+ block = cursor.block()
1749
+ if not block.isValid():
1750
+ logger.debug("Invalid block selected for action '%s'", str(action))
1751
+ return False
1752
+
1753
+ action, text, offset = self._processBlockFormat(action, block.text())
1754
+ if action == nwDocAction.NO_ACTION:
1749
1755
  return False
1750
1756
 
1751
- # Replace the block text
1757
+ pos = cursor.position()
1758
+
1752
1759
  cursor.beginEditBlock()
1753
- posO = cursor.position()
1754
- cursor.select(QTextCursor.SelectionType.BlockUnderCursor)
1755
- posS = cursor.selectionStart()
1756
- cursor.removeSelectedText()
1757
- cursor.setPosition(posS)
1760
+ self._makeSelection(QTextCursor.SelectionType.BlockUnderCursor, cursor)
1761
+ cursor.insertText(text)
1762
+ cursor.endEditBlock()
1763
+
1764
+ if (move := pos - offset) >= 0:
1765
+ cursor.setPosition(move)
1766
+ self.setTextCursor(cursor)
1758
1767
 
1759
- if posS > 0 and hasText:
1760
- # If the block already had text, we must insert a new block
1761
- # first before we can add back the text to it.
1762
- cursor.insertBlock()
1768
+ return True
1763
1769
 
1764
- cursor.insertText(setText)
1770
+ def _iterFormatBlocks(self, action: nwDocAction) -> bool:
1771
+ """Iterate over all selected blocks and apply format. If no
1772
+ selection is made, just forward the call to the single block
1773
+ formatter function.
1774
+ """
1775
+ cursor = self.textCursor()
1776
+ blocks = self._selectedBlocks(cursor)
1777
+ if len(blocks) < 2:
1778
+ return self._formatBlock(action)
1765
1779
 
1766
- if posO - cOffset >= 0:
1767
- cursor.setPosition(posO - cOffset)
1780
+ toggle = True
1781
+ cursor.beginEditBlock()
1782
+ for block in blocks:
1783
+ blockText = block.text()
1784
+ pAction, text, _ = self._processBlockFormat(action, blockText, toggle)
1785
+ if pAction != nwDocAction.NO_ACTION and blockText.strip():
1786
+ action = pAction # First block decides further actions
1787
+ cursor.setPosition(block.position())
1788
+ self._makeSelection(QTextCursor.SelectionType.BlockUnderCursor, cursor)
1789
+ cursor.insertText(text)
1790
+ toggle = False
1768
1791
 
1769
1792
  cursor.endEditBlock()
1770
- self.setTextCursor(cursor)
1771
1793
 
1772
1794
  return True
1773
1795
 
1796
+ def _selectedBlocks(self, cursor: QTextCursor) -> list[QTextBlock]:
1797
+ """Return a list of all blocks selected by a cursor."""
1798
+ if cursor.hasSelection():
1799
+ iS = self._qDocument.findBlock(cursor.selectionStart()).blockNumber()
1800
+ iE = self._qDocument.findBlock(cursor.selectionEnd()).blockNumber()
1801
+ return [self._qDocument.findBlockByNumber(i) for i in range(iS, iE+1)]
1802
+ return []
1803
+
1774
1804
  def _removeInParLineBreaks(self) -> None:
1775
1805
  """Strip line breaks within paragraphs in the selected text."""
1776
1806
  cursor = self.textCursor()
1807
+ if not cursor.hasSelection():
1808
+ cursor.select(QTextCursor.SelectionType.Document)
1777
1809
 
1778
- iS = 0
1779
- iE = self._qDocument.blockCount() - 1
1780
1810
  rS = 0
1781
1811
  rE = self._qDocument.characterCount()
1782
- if cursor.hasSelection():
1783
- sBlock = self._qDocument.findBlock(cursor.selectionStart())
1784
- eBlock = self._qDocument.findBlock(cursor.selectionEnd())
1785
- iS = sBlock.blockNumber()
1786
- iE = eBlock.blockNumber()
1787
- rS = sBlock.position()
1788
- rE = eBlock.position() + eBlock.length()
1812
+ if sBlocks := self._selectedBlocks(cursor):
1813
+ rS = sBlocks[0].position()
1814
+ rE = sBlocks[-1].position() + sBlocks[-1].length()
1789
1815
 
1790
1816
  # Clean up the text
1791
1817
  currPar = []
1792
1818
  cleanText = ""
1793
- for i in range(iS, iE+1):
1794
- cBlock = self._qDocument.findBlockByNumber(i)
1819
+ for cBlock in sBlocks:
1795
1820
  cText = cBlock.text()
1796
1821
  if cText.strip() == "":
1797
1822
  if currPar:
@@ -1839,16 +1864,16 @@ class GuiDocEditor(QPlainTextEdit):
1839
1864
  if len(text) == 0:
1840
1865
  return nwTrinary.NEUTRAL
1841
1866
 
1842
- if text.startswith("@") and isinstance(self._nwItem, NWItem):
1867
+ if text.startswith("@") and self._docHandle:
1843
1868
 
1844
1869
  isGood, tBits, tPos = SHARED.project.index.scanThis(text)
1845
- if not isGood:
1870
+ if not isGood or not tBits or tBits[0] == nwKeyWords.TAG_KEY:
1846
1871
  return nwTrinary.NEUTRAL
1847
1872
 
1848
1873
  tag = ""
1849
1874
  exist = False
1850
1875
  cPos = cursor.selectionStart() - block.position()
1851
- tExist = SHARED.project.index.checkThese(tBits, self._nwItem)
1876
+ tExist = SHARED.project.index.checkThese(tBits, self._docHandle)
1852
1877
  for sTag, sPos, sExist in zip(reversed(tBits), reversed(tPos), reversed(tExist)):
1853
1878
  if cPos >= sPos:
1854
1879
  # The cursor is between the start of two tags
@@ -2045,9 +2070,11 @@ class GuiDocEditor(QPlainTextEdit):
2045
2070
 
2046
2071
  return cursor
2047
2072
 
2048
- def _makeSelection(self, mode: QTextCursor.SelectionType) -> None:
2073
+ def _makeSelection(self, mode: QTextCursor.SelectionType,
2074
+ cursor: QTextCursor | None = None) -> None:
2049
2075
  """Select text based on selection mode."""
2050
- cursor = self.textCursor()
2076
+ if cursor is None:
2077
+ cursor = self.textCursor()
2051
2078
  cursor.clearSelection()
2052
2079
  cursor.select(mode)
2053
2080
 
@@ -2330,9 +2357,9 @@ class GuiDocToolBar(QWidget):
2330
2357
  def updateTheme(self) -> None:
2331
2358
  """Initialise GUI elements that depend on specific settings."""
2332
2359
  palette = QPalette()
2333
- palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
2334
- palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
2335
- palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
2360
+ palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
2361
+ palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
2362
+ palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
2336
2363
  self.setPalette(palette)
2337
2364
 
2338
2365
  self.tbBoldMD.setIcon(SHARED.theme.getIcon("fmt_bold-md"))
@@ -2870,10 +2897,11 @@ class GuiDocEditHeader(QWidget):
2870
2897
  self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
2871
2898
  self.closeButton.setIcon(SHARED.theme.getIcon("close"))
2872
2899
 
2900
+ colText = SHARED.theme.colText
2873
2901
  buttonStyle = (
2874
2902
  "QToolButton {{border: none; background: transparent;}} "
2875
- "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
2876
- ).format(*SHARED.theme.colText)
2903
+ "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
2904
+ ).format(colText.red(), colText.green(), colText.blue())
2877
2905
 
2878
2906
  self.tbButton.setStyleSheet(buttonStyle)
2879
2907
  self.searchButton.setStyleSheet(buttonStyle)
@@ -2889,9 +2917,9 @@ class GuiDocEditHeader(QWidget):
2889
2917
  theme rather than the main GUI.
2890
2918
  """
2891
2919
  palette = QPalette()
2892
- palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
2893
- palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
2894
- palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
2920
+ palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
2921
+ palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
2922
+ palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
2895
2923
 
2896
2924
  self.setPalette(palette)
2897
2925
  self.itemTitle.setPalette(palette)
@@ -3097,9 +3125,9 @@ class GuiDocEditFooter(QWidget):
3097
3125
  theme rather than the main GUI.
3098
3126
  """
3099
3127
  palette = QPalette()
3100
- palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
3101
- palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
3102
- palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText))
3128
+ palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack)
3129
+ palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
3130
+ palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
3103
3131
 
3104
3132
  self.setPalette(palette)
3105
3133
  self.statusText.setPalette(palette)