novelWriter 2.2rc1__py3-none-any.whl → 2.3__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 (162) hide show
  1. {novelWriter-2.2rc1.dist-info → novelWriter-2.3.dist-info}/METADATA +1 -1
  2. {novelWriter-2.2rc1.dist-info → novelWriter-2.3.dist-info}/RECORD +149 -132
  3. {novelWriter-2.2rc1.dist-info → novelWriter-2.3.dist-info}/WHEEL +1 -1
  4. novelWriter-2.3.dist-info/entry_points.txt +2 -0
  5. novelwriter/__init__.py +11 -6
  6. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  7. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  8. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  9. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  10. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  11. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  12. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  13. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  14. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  15. novelwriter/assets/i18n/project_de_DE.json +1 -0
  16. novelwriter/assets/i18n/project_en_US.json +1 -0
  17. novelwriter/assets/i18n/project_es_419.json +11 -0
  18. novelwriter/assets/i18n/project_fr_FR.json +11 -0
  19. novelwriter/assets/i18n/project_it_IT.json +11 -0
  20. novelwriter/assets/i18n/project_ja_JP.json +2 -1
  21. novelwriter/assets/i18n/project_nb_NO.json +1 -0
  22. novelwriter/assets/i18n/project_nl_NL.json +11 -0
  23. novelwriter/assets/i18n/project_pt_BR.json +11 -0
  24. novelwriter/assets/i18n/project_zh_CN.json +11 -0
  25. novelwriter/assets/icons/typicons_dark/icons.conf +11 -2
  26. novelwriter/assets/icons/typicons_dark/mixed_document-new.svg +6 -0
  27. novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
  28. novelwriter/assets/icons/typicons_dark/nw_tb-bold-md.svg +4 -0
  29. novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +3 -1
  30. novelwriter/assets/icons/typicons_dark/nw_tb-italic-md.svg +4 -0
  31. novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +3 -1
  32. novelwriter/assets/icons/typicons_dark/nw_tb-strike-md.svg +4 -0
  33. novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +3 -1
  34. novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +4 -2
  35. novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +4 -2
  36. novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +4 -2
  37. novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
  38. novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
  39. novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
  40. novelwriter/assets/icons/typicons_dark/typ_th-list.svg +9 -0
  41. novelwriter/assets/icons/typicons_light/icons.conf +11 -2
  42. novelwriter/assets/icons/typicons_light/mixed_document-new.svg +6 -0
  43. novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
  44. novelwriter/assets/icons/typicons_light/nw_tb-bold-md.svg +4 -0
  45. novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +3 -1
  46. novelwriter/assets/icons/typicons_light/nw_tb-italic-md.svg +4 -0
  47. novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +3 -1
  48. novelwriter/assets/icons/typicons_light/nw_tb-strike-md.svg +4 -0
  49. novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +3 -1
  50. novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +4 -2
  51. novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +4 -2
  52. novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +4 -2
  53. novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
  54. novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
  55. novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
  56. novelwriter/assets/icons/typicons_light/typ_th-list.svg +9 -0
  57. novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
  58. novelwriter/assets/images/novelwriter-text-light.svg +4 -0
  59. novelwriter/assets/images/welcome-dark.jpg +0 -0
  60. novelwriter/assets/images/welcome-light.jpg +0 -0
  61. novelwriter/assets/manual.pdf +0 -0
  62. novelwriter/assets/sample.zip +0 -0
  63. novelwriter/assets/syntax/cyberpunk_night.conf +26 -0
  64. novelwriter/assets/syntax/default_dark.conf +1 -0
  65. novelwriter/assets/syntax/default_light.conf +1 -0
  66. novelwriter/assets/syntax/grey_dark.conf +1 -0
  67. novelwriter/assets/syntax/grey_light.conf +1 -0
  68. novelwriter/assets/syntax/light_owl.conf +1 -0
  69. novelwriter/assets/syntax/night_owl.conf +1 -0
  70. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  71. novelwriter/assets/syntax/solarized_light.conf +1 -0
  72. novelwriter/assets/syntax/tango.conf +23 -0
  73. novelwriter/assets/syntax/tomorrow.conf +1 -0
  74. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  75. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  76. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  77. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  78. novelwriter/assets/text/credits_en.htm +4 -2
  79. novelwriter/assets/themes/cyberpunk_night.conf +29 -0
  80. novelwriter/assets/themes/default_dark.conf +2 -2
  81. novelwriter/assets/themes/default_light.conf +2 -2
  82. novelwriter/common.py +64 -66
  83. novelwriter/config.py +39 -44
  84. novelwriter/constants.py +39 -17
  85. novelwriter/core/buildsettings.py +8 -8
  86. novelwriter/core/coretools.py +198 -157
  87. novelwriter/core/docbuild.py +7 -4
  88. novelwriter/core/document.py +7 -7
  89. novelwriter/core/index.py +90 -57
  90. novelwriter/core/item.py +23 -5
  91. novelwriter/core/options.py +11 -10
  92. novelwriter/core/project.py +73 -47
  93. novelwriter/core/projectdata.py +3 -16
  94. novelwriter/core/projectxml.py +14 -42
  95. novelwriter/core/sessions.py +4 -3
  96. novelwriter/core/spellcheck.py +6 -4
  97. novelwriter/core/status.py +5 -4
  98. novelwriter/core/storage.py +183 -141
  99. novelwriter/core/tohtml.py +6 -4
  100. novelwriter/core/tokenizer.py +110 -83
  101. novelwriter/core/tomd.py +2 -2
  102. novelwriter/core/toodt.py +41 -31
  103. novelwriter/core/tree.py +5 -4
  104. novelwriter/dialogs/about.py +88 -179
  105. novelwriter/dialogs/docmerge.py +30 -20
  106. novelwriter/dialogs/docsplit.py +33 -22
  107. novelwriter/dialogs/editlabel.py +20 -8
  108. novelwriter/dialogs/preferences.py +562 -725
  109. novelwriter/dialogs/{projsettings.py → projectsettings.py} +301 -270
  110. novelwriter/dialogs/quotes.py +47 -36
  111. novelwriter/dialogs/wordlist.py +128 -59
  112. novelwriter/enum.py +25 -22
  113. novelwriter/error.py +2 -2
  114. novelwriter/extensions/circularprogress.py +12 -12
  115. novelwriter/extensions/configlayout.py +185 -146
  116. novelwriter/extensions/{wheeleventfilter.py → eventfilters.py} +15 -5
  117. novelwriter/extensions/modified.py +81 -0
  118. novelwriter/extensions/novelselector.py +27 -13
  119. novelwriter/extensions/pagedsidebar.py +15 -20
  120. novelwriter/extensions/simpleprogress.py +8 -9
  121. novelwriter/extensions/statusled.py +9 -9
  122. novelwriter/extensions/switch.py +32 -64
  123. novelwriter/extensions/switchbox.py +2 -7
  124. novelwriter/extensions/versioninfo.py +153 -0
  125. novelwriter/gui/doceditor.py +250 -214
  126. novelwriter/gui/dochighlight.py +66 -94
  127. novelwriter/gui/docviewer.py +71 -98
  128. novelwriter/gui/docviewerpanel.py +140 -47
  129. novelwriter/gui/editordocument.py +3 -3
  130. novelwriter/gui/itemdetails.py +9 -9
  131. novelwriter/gui/mainmenu.py +47 -47
  132. novelwriter/gui/noveltree.py +53 -61
  133. novelwriter/gui/outline.py +100 -76
  134. novelwriter/gui/projtree.py +246 -112
  135. novelwriter/gui/sidebar.py +9 -8
  136. novelwriter/gui/statusbar.py +49 -7
  137. novelwriter/gui/theme.py +74 -76
  138. novelwriter/guimain.py +175 -330
  139. novelwriter/shared.py +68 -30
  140. novelwriter/tools/dictionaries.py +7 -8
  141. novelwriter/tools/lipsum.py +34 -28
  142. novelwriter/tools/manusbuild.py +3 -4
  143. novelwriter/tools/manuscript.py +25 -32
  144. novelwriter/tools/manussettings.py +194 -225
  145. novelwriter/tools/noveldetails.py +525 -0
  146. novelwriter/tools/welcome.py +819 -0
  147. novelwriter/tools/writingstats.py +26 -13
  148. novelWriter-2.2rc1.dist-info/entry_points.txt +0 -5
  149. novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +0 -8
  150. novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +0 -8
  151. novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +0 -8
  152. novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +0 -8
  153. novelwriter/assets/images/wizard-back.jpg +0 -0
  154. novelwriter/assets/text/gplv3_en.htm +0 -641
  155. novelwriter/assets/text/release_notes.htm +0 -17
  156. novelwriter/dialogs/projdetails.py +0 -525
  157. novelwriter/dialogs/projload.py +0 -298
  158. novelwriter/dialogs/updates.py +0 -182
  159. novelwriter/extensions/pageddialog.py +0 -130
  160. novelwriter/tools/projwizard.py +0 -478
  161. {novelWriter-2.2rc1.dist-info → novelWriter-2.3.dist-info}/LICENSE.md +0 -0
  162. {novelWriter-2.2rc1.dist-info → novelWriter-2.3.dist-info}/top_level.txt +0 -0
@@ -14,7 +14,7 @@ Created: 2023-11-06 [2.2b1] MetaCompleter
14
14
  Created: 2023-11-07 [2.2b1] GuiDocToolBar
15
15
 
16
16
  This file is a part of novelWriter
17
- Copyright 2018–2023, Veronica Berglyd Olsen
17
+ Copyright 2018–2024, Veronica Berglyd Olsen
18
18
 
19
19
  This program is free software: you can redistribute it and/or modify
20
20
  it under the terms of the GNU General Public License as published by
@@ -56,12 +56,12 @@ 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
60
+ from novelwriter.tools.lipsum import GuiLipsum
61
61
  from novelwriter.core.document import NWDocument
62
62
  from novelwriter.gui.dochighlight import GuiDocHighlighter
63
63
  from novelwriter.gui.editordocument import GuiTextDocument
64
- from novelwriter.extensions.wheeleventfilter import WheelEventFilter
64
+ from novelwriter.extensions.eventfilters import WheelEventFilter
65
65
 
66
66
  if TYPE_CHECKING: # pragma: no cover
67
67
  from novelwriter.guimain import GuiMain
@@ -186,13 +186,12 @@ class GuiDocEditor(QPlainTextEdit):
186
186
  # Set Up Document Word Counter
187
187
  self.wcTimerDoc = QTimer()
188
188
  self.wcTimerDoc.timeout.connect(self._runDocCounter)
189
+ self.wcTimerDoc.setInterval(5000)
189
190
 
190
191
  self.wCounterDoc = BackgroundWordCounter(self)
191
192
  self.wCounterDoc.setAutoDelete(False)
192
193
  self.wCounterDoc.signals.countsReady.connect(self._updateDocCounts)
193
194
 
194
- self.wcInterval = CONFIG.wordCountTimer
195
-
196
195
  # Set Up Selection Word Counter
197
196
  self.wcTimerSel = QTimer()
198
197
  self.wcTimerSel.timeout.connect(self._runSelCounter)
@@ -276,14 +275,14 @@ class GuiDocEditor(QPlainTextEdit):
276
275
  def updateSyntaxColours(self) -> None:
277
276
  """Update the syntax highlighting theme."""
278
277
  mainPalette = self.palette()
279
- mainPalette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
280
- mainPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack))
281
- 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)
282
281
  self.setPalette(mainPalette)
283
282
 
284
283
  docPalette = self.viewport().palette()
285
- docPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack))
286
- 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)
287
286
  self.viewport().setPalette(docPalette)
288
287
 
289
288
  self.docHeader.matchColours()
@@ -319,10 +318,10 @@ class GuiDocEditor(QPlainTextEdit):
319
318
  SHARED.updateSpellCheckLanguage()
320
319
 
321
320
  # Set font
322
- textFont = QFont()
323
- textFont.setFamily(CONFIG.textFont)
324
- textFont.setPointSize(CONFIG.textSize)
325
- self.setFont(textFont)
321
+ font = QFont()
322
+ font.setFamily(CONFIG.textFont)
323
+ font.setPointSize(CONFIG.textSize)
324
+ self.setFont(font)
326
325
 
327
326
  # Set default text margins
328
327
  # Due to cursor visibility, a part of the margin must be
@@ -358,17 +357,14 @@ class GuiDocEditor(QPlainTextEdit):
358
357
  # Refresh the tab stops
359
358
  self.setTabStopDistance(CONFIG.getTabWidth())
360
359
 
361
- # Configure word count timer
362
- self.wcInterval = CONFIG.wordCountTimer
363
- self.wcTimerDoc.setInterval(int(self.wcInterval*1000))
364
-
365
- # 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
366
361
  # font changed, otherwise we just clear the editor entirely,
367
362
  # which makes it read only.
368
- if self._docHandle is None:
369
- self.clearEditor()
370
- else:
363
+ if self._docHandle:
371
364
  self._qDocument.syntaxHighlighter.rehighlight()
365
+ self.docHeader.setTitleFromHandle(self._docHandle)
366
+ else:
367
+ self.clearEditor()
372
368
 
373
369
  return
374
370
 
@@ -717,11 +713,11 @@ class GuiDocEditor(QPlainTextEdit):
717
713
  self.copy()
718
714
  elif action == nwDocAction.PASTE:
719
715
  self.paste()
720
- elif action == nwDocAction.EMPH:
716
+ elif action == nwDocAction.MD_ITALIC:
721
717
  self._toggleFormat(1, "_")
722
- elif action == nwDocAction.STRONG:
718
+ elif action == nwDocAction.MD_BOLD:
723
719
  self._toggleFormat(2, "*")
724
- elif action == nwDocAction.STRIKE:
720
+ elif action == nwDocAction.MD_STRIKE:
725
721
  self._toggleFormat(2, "~")
726
722
  elif action == nwDocAction.S_QUOTE:
727
723
  self._wrapSelection(self._typSQuoteO, self._typSQuoteC)
@@ -740,9 +736,11 @@ class GuiDocEditor(QPlainTextEdit):
740
736
  elif action == nwDocAction.BLOCK_H4:
741
737
  self._formatBlock(nwDocAction.BLOCK_H4)
742
738
  elif action == nwDocAction.BLOCK_COM:
743
- self._formatBlock(nwDocAction.BLOCK_COM)
739
+ self._iterFormatBlocks(nwDocAction.BLOCK_COM)
740
+ elif action == nwDocAction.BLOCK_IGN:
741
+ self._iterFormatBlocks(nwDocAction.BLOCK_IGN)
744
742
  elif action == nwDocAction.BLOCK_TXT:
745
- self._formatBlock(nwDocAction.BLOCK_TXT)
743
+ self._iterFormatBlocks(nwDocAction.BLOCK_TXT)
746
744
  elif action == nwDocAction.BLOCK_TTL:
747
745
  self._formatBlock(nwDocAction.BLOCK_TTL)
748
746
  elif action == nwDocAction.BLOCK_UNN:
@@ -831,11 +829,11 @@ class GuiDocEditor(QPlainTextEdit):
831
829
  elif insert == nwDocInsert.QUOTE_RD:
832
830
  text = self._typDQuoteC
833
831
  elif insert == nwDocInsert.SYNOPSIS:
834
- text = "% Synopsis: "
832
+ text = "%Synopsis: "
835
833
  newBlock = True
836
834
  goAfter = True
837
835
  elif insert == nwDocInsert.SHORT:
838
- text = "% Short: "
836
+ text = "%Short: "
839
837
  newBlock = True
840
838
  goAfter = True
841
839
  elif insert == nwDocInsert.NEW_PAGE:
@@ -850,18 +848,23 @@ class GuiDocEditor(QPlainTextEdit):
850
848
  text = "[vspace:2]"
851
849
  newBlock = True
852
850
  goAfter = False
851
+ elif insert == nwDocInsert.LIPSUM:
852
+ text = GuiLipsum.getLipsum(self)
853
+ newBlock = True
854
+ goAfter = False
853
855
  else:
854
856
  return False
855
857
  else:
856
858
  return False
857
859
 
858
- if newBlock:
859
- self.insertNewBlock(text, defaultAfter=goAfter)
860
- else:
861
- cursor = self.textCursor()
862
- cursor.beginEditBlock()
863
- cursor.insertText(text)
864
- cursor.endEditBlock()
860
+ if text:
861
+ if newBlock:
862
+ self.insertNewBlock(text, defaultAfter=goAfter)
863
+ else:
864
+ cursor = self.textCursor()
865
+ cursor.beginEditBlock()
866
+ cursor.insertText(text)
867
+ cursor.endEditBlock()
865
868
 
866
869
  return True
867
870
 
@@ -1144,6 +1147,7 @@ class GuiDocEditor(QPlainTextEdit):
1144
1147
 
1145
1148
  # Execute the context menu
1146
1149
  ctxMenu.exec_(self.viewport().mapToGlobal(pos))
1150
+ ctxMenu.deleteLater()
1147
1151
 
1148
1152
  return
1149
1153
 
@@ -1183,7 +1187,7 @@ class GuiDocEditor(QPlainTextEdit):
1183
1187
  logger.debug("Word counter is busy")
1184
1188
  return
1185
1189
 
1186
- if time() - self._lastEdit < 5.0 * self.wcInterval:
1190
+ if time() - self._lastEdit < 25.0:
1187
1191
  logger.debug("Running word counter")
1188
1192
  SHARED.runInThreadPool(self.wCounterDoc)
1189
1193
 
@@ -1586,9 +1590,7 @@ class GuiDocEditor(QPlainTextEdit):
1586
1590
 
1587
1591
  posS = cursor.selectionStart()
1588
1592
  posE = cursor.selectionEnd()
1589
- closeCheck = (
1590
- " ", "\n", nwUnicode.U_LSEP, nwUnicode.U_PSEP
1591
- )
1593
+ closeCheck = (" ", "\n", nwUnicode.U_LSEP, nwUnicode.U_PSEP)
1592
1594
 
1593
1595
  self._allowAutoReplace(False)
1594
1596
  for posC in range(posS, posE+1):
@@ -1632,159 +1634,189 @@ class GuiDocEditor(QPlainTextEdit):
1632
1634
 
1633
1635
  return True
1634
1636
 
1635
- def _formatBlock(self, action: nwDocAction) -> bool:
1636
- """Change the block format of the block under the cursor."""
1637
- cursor = self.textCursor()
1638
- block = cursor.block()
1639
- if not block.isValid():
1640
- logger.debug("Invalid block selected for action '%s'", str(action))
1641
- return False
1642
-
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."""
1643
1641
  # Remove existing format first, if any
1644
- setText = block.text()
1645
- hasText = len(setText) > 0
1646
- if setText.startswith("@"):
1642
+ if text.startswith("@"):
1647
1643
  logger.error("Cannot apply block format to keyword/value line")
1648
- return False
1649
- elif setText.startswith("% "):
1650
- newText = setText[2:]
1651
- cOffset = 2
1652
- 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:
1653
1649
  action = nwDocAction.BLOCK_TXT
1654
- elif setText.startswith("%"):
1655
- newText = setText[1:]
1656
- cOffset = 1
1657
- 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:
1658
1654
  action = nwDocAction.BLOCK_TXT
1659
- elif setText.startswith("# "):
1660
- newText = setText[2:]
1661
- cOffset = 2
1662
- elif setText.startswith("## "):
1663
- newText = setText[3:]
1664
- cOffset = 3
1665
- elif setText.startswith("### "):
1666
- newText = setText[4:]
1667
- cOffset = 4
1668
- elif setText.startswith("#### "):
1669
- newText = setText[5:]
1670
- cOffset = 5
1671
- elif setText.startswith("#! "):
1672
- newText = setText[3:]
1673
- cOffset = 3
1674
- elif setText.startswith("##! "):
1675
- newText = setText[4:]
1676
- cOffset = 4
1677
- elif setText.startswith(">> "):
1678
- newText = setText[3:]
1679
- cOffset = 3
1680
- elif setText.startswith("> ") and action != nwDocAction.INDENT_R:
1681
- newText = setText[2:]
1682
- cOffset = 2
1683
- elif setText.startswith(">>"):
1684
- newText = setText[2:]
1685
- cOffset = 2
1686
- elif setText.startswith(">") and action != nwDocAction.INDENT_R:
1687
- newText = setText[1:]
1688
- 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
1689
1685
  else:
1690
- newText = setText
1691
- cOffset = 0
1686
+ temp = text
1687
+ offset = 0
1692
1688
 
1693
1689
  # Also remove formatting tags at the end
1694
- if setText.endswith(" <<"):
1695
- newText = newText[:-3]
1696
- elif setText.endswith(" <") and action != nwDocAction.INDENT_L:
1697
- newText = newText[:-2]
1698
- elif setText.endswith("<<"):
1699
- newText = newText[:-2]
1700
- elif setText.endswith("<") and action != nwDocAction.INDENT_L:
1701
- 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]
1702
1698
 
1703
1699
  # Apply new format
1704
1700
  if action == nwDocAction.BLOCK_COM:
1705
- setText = "% "+newText
1706
- cOffset -= 2
1701
+ text = f"% {temp}"
1702
+ offset -= 2
1703
+ elif action == nwDocAction.BLOCK_IGN:
1704
+ text = f"%~ {temp}"
1705
+ offset -= 3
1707
1706
  elif action == nwDocAction.BLOCK_H1:
1708
- setText = "# "+newText
1709
- cOffset -= 2
1707
+ text = f"# {temp}"
1708
+ offset -= 2
1710
1709
  elif action == nwDocAction.BLOCK_H2:
1711
- setText = "## "+newText
1712
- cOffset -= 3
1710
+ text = f"## {temp}"
1711
+ offset -= 3
1713
1712
  elif action == nwDocAction.BLOCK_H3:
1714
- setText = "### "+newText
1715
- cOffset -= 4
1713
+ text = f"### {temp}"
1714
+ offset -= 4
1716
1715
  elif action == nwDocAction.BLOCK_H4:
1717
- setText = "#### "+newText
1718
- cOffset -= 5
1716
+ text = f"#### {temp}"
1717
+ offset -= 5
1719
1718
  elif action == nwDocAction.BLOCK_TTL:
1720
- setText = "#! "+newText
1721
- cOffset -= 3
1719
+ text = f"#! {temp}"
1720
+ offset -= 3
1722
1721
  elif action == nwDocAction.BLOCK_UNN:
1723
- setText = "##! "+newText
1724
- cOffset -= 4
1722
+ text = f"##! {temp}"
1723
+ offset -= 4
1725
1724
  elif action == nwDocAction.ALIGN_L:
1726
- setText = newText+" <<"
1725
+ text = f"{temp} <<"
1727
1726
  elif action == nwDocAction.ALIGN_C:
1728
- setText = ">> "+newText+" <<"
1729
- cOffset -= 3
1727
+ text = f">> {temp} <<"
1728
+ offset -= 3
1730
1729
  elif action == nwDocAction.ALIGN_R:
1731
- setText = ">> "+newText
1732
- cOffset -= 3
1730
+ text = f">> {temp}"
1731
+ offset -= 3
1733
1732
  elif action == nwDocAction.INDENT_L:
1734
- setText = "> "+newText
1735
- cOffset -= 2
1733
+ text = f"> {temp}"
1734
+ offset -= 2
1736
1735
  elif action == nwDocAction.INDENT_R:
1737
- setText = newText+" <"
1736
+ text = f"{temp} <"
1738
1737
  elif action == nwDocAction.BLOCK_TXT:
1739
- setText = newText
1738
+ text = temp
1740
1739
  else:
1741
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:
1742
1755
  return False
1743
1756
 
1744
- # Replace the block text
1757
+ pos = cursor.position()
1758
+
1745
1759
  cursor.beginEditBlock()
1746
- posO = cursor.position()
1747
- cursor.select(QTextCursor.SelectionType.BlockUnderCursor)
1748
- posS = cursor.selectionStart()
1749
- cursor.removeSelectedText()
1750
- cursor.setPosition(posS)
1760
+ self._makeSelection(QTextCursor.SelectionType.BlockUnderCursor, cursor)
1761
+ cursor.insertText(text)
1762
+ cursor.endEditBlock()
1751
1763
 
1752
- if posS > 0 and hasText:
1753
- # If the block already had text, we must insert a new block
1754
- # first before we can add back the text to it.
1755
- cursor.insertBlock()
1764
+ if (move := pos - offset) >= 0:
1765
+ cursor.setPosition(move)
1766
+ self.setTextCursor(cursor)
1756
1767
 
1757
- cursor.insertText(setText)
1768
+ return True
1758
1769
 
1759
- if posO - cOffset >= 0:
1760
- cursor.setPosition(posO - cOffset)
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)
1779
+
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
1761
1791
 
1762
1792
  cursor.endEditBlock()
1763
- self.setTextCursor(cursor)
1764
1793
 
1765
1794
  return True
1766
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
+
1767
1804
  def _removeInParLineBreaks(self) -> None:
1768
1805
  """Strip line breaks within paragraphs in the selected text."""
1769
1806
  cursor = self.textCursor()
1807
+ if not cursor.hasSelection():
1808
+ cursor.select(QTextCursor.SelectionType.Document)
1770
1809
 
1771
- iS = 0
1772
- iE = self._qDocument.blockCount() - 1
1773
1810
  rS = 0
1774
1811
  rE = self._qDocument.characterCount()
1775
- if cursor.hasSelection():
1776
- sBlock = self._qDocument.findBlock(cursor.selectionStart())
1777
- eBlock = self._qDocument.findBlock(cursor.selectionEnd())
1778
- iS = sBlock.blockNumber()
1779
- iE = eBlock.blockNumber()
1780
- rS = sBlock.position()
1781
- rE = eBlock.position() + eBlock.length()
1812
+ if sBlocks := self._selectedBlocks(cursor):
1813
+ rS = sBlocks[0].position()
1814
+ rE = sBlocks[-1].position() + sBlocks[-1].length()
1782
1815
 
1783
1816
  # Clean up the text
1784
1817
  currPar = []
1785
1818
  cleanText = ""
1786
- for i in range(iS, iE+1):
1787
- cBlock = self._qDocument.findBlockByNumber(i)
1819
+ for cBlock in sBlocks:
1788
1820
  cText = cBlock.text()
1789
1821
  if cText.strip() == "":
1790
1822
  if currPar:
@@ -1832,16 +1864,16 @@ class GuiDocEditor(QPlainTextEdit):
1832
1864
  if len(text) == 0:
1833
1865
  return nwTrinary.NEUTRAL
1834
1866
 
1835
- if text.startswith("@") and isinstance(self._nwItem, NWItem):
1867
+ if text.startswith("@") and self._docHandle:
1836
1868
 
1837
1869
  isGood, tBits, tPos = SHARED.project.index.scanThis(text)
1838
- if not isGood:
1870
+ if not isGood or not tBits or tBits[0] == nwKeyWords.TAG_KEY:
1839
1871
  return nwTrinary.NEUTRAL
1840
1872
 
1841
1873
  tag = ""
1842
1874
  exist = False
1843
1875
  cPos = cursor.selectionStart() - block.position()
1844
- tExist = SHARED.project.index.checkThese(tBits, self._nwItem)
1876
+ tExist = SHARED.project.index.checkThese(tBits, self._docHandle)
1845
1877
  for sTag, sPos, sExist in zip(reversed(tBits), reversed(tPos), reversed(tExist)):
1846
1878
  if cPos >= sPos:
1847
1879
  # The cursor is between the start of two tags
@@ -2005,20 +2037,25 @@ class GuiDocEditor(QPlainTextEdit):
2005
2037
  cPos = cursor.position()
2006
2038
  bPos = cursor.block().position()
2007
2039
  bLen = cursor.block().length()
2040
+ apos = nwUnicode.U_APOS + nwUnicode.U_RSQUO
2008
2041
 
2009
- # Scan backwards
2042
+ # Scan backward
2010
2043
  sPos = cPos
2011
2044
  for i in range(cPos - bPos):
2012
2045
  sPos = cPos - i - 1
2013
- if not self._qDocument.characterAt(sPos).isalnum():
2046
+ cOne = self._qDocument.characterAt(sPos)
2047
+ cTwo = self._qDocument.characterAt(sPos - 1)
2048
+ if not (cOne.isalnum() or cOne in apos and cTwo.isalnum()):
2014
2049
  sPos += 1
2015
2050
  break
2016
2051
 
2017
- # Scan forwards
2052
+ # Scan forward
2018
2053
  ePos = cPos
2019
2054
  for i in range(bPos + bLen - cPos):
2020
2055
  ePos = cPos + i
2021
- if not self._qDocument.characterAt(ePos).isalnum():
2056
+ cOne = self._qDocument.characterAt(ePos)
2057
+ cTwo = self._qDocument.characterAt(ePos + 1)
2058
+ if not (cOne.isalnum() or cOne in apos and cTwo.isalnum()):
2022
2059
  break
2023
2060
 
2024
2061
  if ePos - sPos <= 0:
@@ -2033,9 +2070,11 @@ class GuiDocEditor(QPlainTextEdit):
2033
2070
 
2034
2071
  return cursor
2035
2072
 
2036
- def _makeSelection(self, mode: QTextCursor.SelectionType) -> None:
2073
+ def _makeSelection(self, mode: QTextCursor.SelectionType,
2074
+ cursor: QTextCursor | None = None) -> None:
2037
2075
  """Select text based on selection mode."""
2038
- cursor = self.textCursor()
2076
+ if cursor is None:
2077
+ cursor = self.textCursor()
2039
2078
  cursor.clearSelection()
2040
2079
  cursor.select(mode)
2041
2080
 
@@ -2225,39 +2264,65 @@ class GuiDocToolBar(QWidget):
2225
2264
  # General Buttons
2226
2265
  # ===============
2227
2266
 
2228
- self.tbMode = QToolButton(self)
2229
- self.tbMode.setToolTip(self.tr("Toggle Markdown or Shortcodes Mode"))
2230
- self.tbMode.setIconSize(iconSize)
2231
- self.tbMode.setCheckable(True)
2232
- self.tbMode.setChecked(CONFIG.useShortcodes)
2233
- self.tbMode.toggled.connect(self._toggleFormatMode)
2267
+ self.tbBoldMD = QToolButton(self)
2268
+ self.tbBoldMD.setIconSize(iconSize)
2269
+ self.tbBoldMD.setToolTip(self.tr("Markdown Bold"))
2270
+ self.tbBoldMD.clicked.connect(
2271
+ lambda: self.requestDocAction.emit(nwDocAction.MD_BOLD)
2272
+ )
2273
+
2274
+ self.tbItalicMD = QToolButton(self)
2275
+ self.tbItalicMD.setIconSize(iconSize)
2276
+ self.tbItalicMD.setToolTip(self.tr("Markdown Italic"))
2277
+ self.tbItalicMD.clicked.connect(
2278
+ lambda: self.requestDocAction.emit(nwDocAction.MD_ITALIC)
2279
+ )
2280
+
2281
+ self.tbStrikeMD = QToolButton(self)
2282
+ self.tbStrikeMD.setIconSize(iconSize)
2283
+ self.tbStrikeMD.setToolTip(self.tr("Markdown Strikethrough"))
2284
+ self.tbStrikeMD.clicked.connect(
2285
+ lambda: self.requestDocAction.emit(nwDocAction.MD_STRIKE)
2286
+ )
2234
2287
 
2235
2288
  self.tbBold = QToolButton(self)
2236
2289
  self.tbBold.setIconSize(iconSize)
2237
- self.tbBold.clicked.connect(self._formatBold)
2290
+ self.tbBold.setToolTip(self.tr("Shortcode Bold"))
2291
+ self.tbBold.clicked.connect(
2292
+ lambda: self.requestDocAction.emit(nwDocAction.SC_BOLD)
2293
+ )
2238
2294
 
2239
2295
  self.tbItalic = QToolButton(self)
2240
2296
  self.tbItalic.setIconSize(iconSize)
2241
- self.tbItalic.clicked.connect(self._formatItalic)
2297
+ self.tbItalic.setToolTip(self.tr("Shortcode Italic"))
2298
+ self.tbItalic.clicked.connect(
2299
+ lambda: self.requestDocAction.emit(nwDocAction.SC_ITALIC)
2300
+ )
2242
2301
 
2243
2302
  self.tbStrike = QToolButton(self)
2244
2303
  self.tbStrike.setIconSize(iconSize)
2245
- self.tbStrike.clicked.connect(self._formatStrike)
2304
+ self.tbStrike.setToolTip(self.tr("Shortcode Strikethrough"))
2305
+ self.tbStrike.clicked.connect(
2306
+ lambda: self.requestDocAction.emit(nwDocAction.SC_STRIKE)
2307
+ )
2246
2308
 
2247
2309
  self.tbUnderline = QToolButton(self)
2248
2310
  self.tbUnderline.setIconSize(iconSize)
2311
+ self.tbUnderline.setToolTip(self.tr("Shortcode Underline"))
2249
2312
  self.tbUnderline.clicked.connect(
2250
2313
  lambda: self.requestDocAction.emit(nwDocAction.SC_ULINE)
2251
2314
  )
2252
2315
 
2253
2316
  self.tbSuperscript = QToolButton(self)
2254
2317
  self.tbSuperscript.setIconSize(iconSize)
2318
+ self.tbSuperscript.setToolTip(self.tr("Shortcode Superscript"))
2255
2319
  self.tbSuperscript.clicked.connect(
2256
2320
  lambda: self.requestDocAction.emit(nwDocAction.SC_SUP)
2257
2321
  )
2258
2322
 
2259
2323
  self.tbSubscript = QToolButton(self)
2260
2324
  self.tbSubscript.setIconSize(iconSize)
2325
+ self.tbSubscript.setToolTip(self.tr("Shortcode Subscript"))
2261
2326
  self.tbSubscript.clicked.connect(
2262
2327
  lambda: self.requestDocAction.emit(nwDocAction.SC_SUB)
2263
2328
  )
@@ -2266,7 +2331,10 @@ class GuiDocToolBar(QWidget):
2266
2331
  # ========
2267
2332
 
2268
2333
  self.outerBox = QVBoxLayout()
2269
- self.outerBox.addWidget(self.tbMode)
2334
+ self.outerBox.addWidget(self.tbBoldMD)
2335
+ self.outerBox.addWidget(self.tbItalicMD)
2336
+ self.outerBox.addWidget(self.tbStrikeMD)
2337
+ self.outerBox.addSpacing(cM)
2270
2338
  self.outerBox.addWidget(self.tbBold)
2271
2339
  self.outerBox.addWidget(self.tbItalic)
2272
2340
  self.outerBox.addWidget(self.tbStrike)
@@ -2289,13 +2357,14 @@ class GuiDocToolBar(QWidget):
2289
2357
  def updateTheme(self) -> None:
2290
2358
  """Initialise GUI elements that depend on specific settings."""
2291
2359
  palette = QPalette()
2292
- palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
2293
- palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
2294
- 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)
2295
2363
  self.setPalette(palette)
2296
2364
 
2297
- tPx = int(0.8*SHARED.theme.fontPixelSize)
2298
- self.tbMode.setIcon(SHARED.theme.getToggleIcon("fmt_mode", (tPx, tPx)))
2365
+ self.tbBoldMD.setIcon(SHARED.theme.getIcon("fmt_bold-md"))
2366
+ self.tbItalicMD.setIcon(SHARED.theme.getIcon("fmt_italic-md"))
2367
+ self.tbStrikeMD.setIcon(SHARED.theme.getIcon("fmt_strike-md"))
2299
2368
  self.tbBold.setIcon(SHARED.theme.getIcon("fmt_bold"))
2300
2369
  self.tbItalic.setIcon(SHARED.theme.getIcon("fmt_italic"))
2301
2370
  self.tbStrike.setIcon(SHARED.theme.getIcon("fmt_strike"))
@@ -2305,40 +2374,6 @@ class GuiDocToolBar(QWidget):
2305
2374
 
2306
2375
  return
2307
2376
 
2308
- ##
2309
- # Private Slots
2310
- ##
2311
-
2312
- @pyqtSlot(bool)
2313
- def _toggleFormatMode(self, checked: bool) -> None:
2314
- """Toggle the formatting mode."""
2315
- CONFIG.useShortcodes = checked
2316
- return
2317
-
2318
- @pyqtSlot()
2319
- def _formatBold(self):
2320
- """Call the bold format action."""
2321
- self.requestDocAction.emit(
2322
- nwDocAction.SC_BOLD if self.tbMode.isChecked() else nwDocAction.STRONG
2323
- )
2324
- return
2325
-
2326
- @pyqtSlot()
2327
- def _formatItalic(self):
2328
- """Call the italic format action."""
2329
- self.requestDocAction.emit(
2330
- nwDocAction.SC_ITALIC if self.tbMode.isChecked() else nwDocAction.EMPH
2331
- )
2332
- return
2333
-
2334
- @pyqtSlot()
2335
- def _formatStrike(self):
2336
- """Call the strikethrough format action."""
2337
- self.requestDocAction.emit(
2338
- nwDocAction.SC_STRIKE if self.tbMode.isChecked() else nwDocAction.STRIKE
2339
- )
2340
- return
2341
-
2342
2377
  # END Class GuiDocToolBar
2343
2378
 
2344
2379
 
@@ -2862,10 +2897,11 @@ class GuiDocEditHeader(QWidget):
2862
2897
  self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
2863
2898
  self.closeButton.setIcon(SHARED.theme.getIcon("close"))
2864
2899
 
2900
+ colText = SHARED.theme.colText
2865
2901
  buttonStyle = (
2866
2902
  "QToolButton {{border: none; background: transparent;}} "
2867
- "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
2868
- ).format(*SHARED.theme.colText)
2903
+ "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
2904
+ ).format(colText.red(), colText.green(), colText.blue())
2869
2905
 
2870
2906
  self.tbButton.setStyleSheet(buttonStyle)
2871
2907
  self.searchButton.setStyleSheet(buttonStyle)
@@ -2881,9 +2917,9 @@ class GuiDocEditHeader(QWidget):
2881
2917
  theme rather than the main GUI.
2882
2918
  """
2883
2919
  palette = QPalette()
2884
- palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
2885
- palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
2886
- 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)
2887
2923
 
2888
2924
  self.setPalette(palette)
2889
2925
  self.itemTitle.setPalette(palette)
@@ -3089,9 +3125,9 @@ class GuiDocEditFooter(QWidget):
3089
3125
  theme rather than the main GUI.
3090
3126
  """
3091
3127
  palette = QPalette()
3092
- palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack))
3093
- palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText))
3094
- 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)
3095
3131
 
3096
3132
  self.setPalette(palette)
3097
3133
  self.statusText.setPalette(palette)