novelWriter 2.6rc1__py3-none-any.whl → 2.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {novelWriter-2.6rc1.dist-info → novelWriter-2.6.1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.6rc1.dist-info → novelWriter-2.6.1.dist-info}/RECORD +57 -56
  3. novelwriter/__init__.py +3 -3
  4. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  5. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  6. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  7. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  8. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  9. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  10. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  11. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  12. novelwriter/assets/i18n/nw_pl_PL.qm +0 -0
  13. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  14. novelwriter/assets/i18n/nw_ru_RU.qm +0 -0
  15. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  16. novelwriter/assets/i18n/project_de_DE.json +2 -0
  17. novelwriter/assets/i18n/project_en_US.json +2 -0
  18. novelwriter/assets/i18n/project_es_419.json +2 -0
  19. novelwriter/assets/i18n/project_fr_FR.json +3 -1
  20. novelwriter/assets/i18n/project_it_IT.json +2 -0
  21. novelwriter/assets/i18n/project_ja_JP.json +2 -0
  22. novelwriter/assets/i18n/project_nb_NO.json +2 -0
  23. novelwriter/assets/i18n/project_nl_NL.json +2 -0
  24. novelwriter/assets/i18n/project_pl_PL.json +2 -0
  25. novelwriter/assets/i18n/project_pt_BR.json +2 -0
  26. novelwriter/assets/i18n/project_zh_CN.json +2 -0
  27. novelwriter/assets/manual.pdf +0 -0
  28. novelwriter/assets/manual_fr_FR.pdf +0 -0
  29. novelwriter/assets/sample.zip +0 -0
  30. novelwriter/config.py +41 -19
  31. novelwriter/constants.py +4 -0
  32. novelwriter/core/buildsettings.py +7 -0
  33. novelwriter/core/project.py +2 -6
  34. novelwriter/dialogs/preferences.py +14 -5
  35. novelwriter/enum.py +0 -7
  36. novelwriter/extensions/novelselector.py +3 -2
  37. novelwriter/extensions/statusled.py +5 -6
  38. novelwriter/formats/shared.py +12 -11
  39. novelwriter/formats/todocx.py +1 -1
  40. novelwriter/formats/tohtml.py +30 -27
  41. novelwriter/formats/tokenizer.py +14 -8
  42. novelwriter/formats/tomarkdown.py +3 -2
  43. novelwriter/formats/toodt.py +1 -1
  44. novelwriter/formats/toqdoc.py +2 -0
  45. novelwriter/gui/doceditor.py +35 -19
  46. novelwriter/gui/docviewer.py +1 -1
  47. novelwriter/gui/itemdetails.py +1 -0
  48. novelwriter/gui/projtree.py +15 -18
  49. novelwriter/gui/statusbar.py +6 -7
  50. novelwriter/guimain.py +1 -1
  51. novelwriter/tools/dictionaries.py +2 -2
  52. novelwriter/tools/manussettings.py +8 -8
  53. novelwriter/tools/welcome.py +1 -1
  54. {novelWriter-2.6rc1.dist-info → novelWriter-2.6.1.dist-info}/LICENSE.md +0 -0
  55. {novelWriter-2.6rc1.dist-info → novelWriter-2.6.1.dist-info}/WHEEL +0 -0
  56. {novelWriter-2.6rc1.dist-info → novelWriter-2.6.1.dist-info}/entry_points.txt +0 -0
  57. {novelWriter-2.6rc1.dist-info → novelWriter-2.6.1.dist-info}/top_level.txt +0 -0
@@ -271,10 +271,10 @@ class GuiPreferences(NDialog):
271
271
  self.tr("Include project notes in status bar word count"), self.incNotesWCount
272
272
  )
273
273
 
274
- # Auto Save
274
+ # Behaviour
275
275
  # =========
276
276
 
277
- title = self.tr("Auto Save")
277
+ title = self.tr("Behaviour")
278
278
  section += 1
279
279
  self.sidebar.addButton(title, section)
280
280
  self.mainForm.addGroupLabel(title, section)
@@ -301,6 +301,14 @@ class GuiPreferences(NDialog):
301
301
  self.tr("How often the project is automatically saved."), unit=self.tr("seconds")
302
302
  )
303
303
 
304
+ # Ask before exiting novelWriter
305
+ self.askBeforeExit = NSwitch(self)
306
+ self.askBeforeExit.setChecked(CONFIG.askBeforeExit)
307
+ self.mainForm.addRow(
308
+ self.tr("Ask before exiting novelWriter"), self.askBeforeExit,
309
+ self.tr("Only applies when a project is open.")
310
+ )
311
+
304
312
  # Project Backup
305
313
  # ==============
306
314
 
@@ -927,9 +935,10 @@ class GuiPreferences(NDialog):
927
935
  CONFIG.incNotesWCount = self.incNotesWCount.isChecked()
928
936
  CONFIG.setTextFont(self._textFont)
929
937
 
930
- # Auto Save
931
- CONFIG.autoSaveDoc = self.autoSaveDoc.value()
932
- CONFIG.autoSaveProj = self.autoSaveProj.value()
938
+ # Behaviour
939
+ CONFIG.autoSaveDoc = self.autoSaveDoc.value()
940
+ CONFIG.autoSaveProj = self.autoSaveProj.value()
941
+ CONFIG.askBeforeExit = self.askBeforeExit.isChecked()
933
942
 
934
943
  # Project Backup
935
944
  CONFIG.setBackupPath(self.backupPath)
novelwriter/enum.py CHANGED
@@ -68,13 +68,6 @@ class nwComment(Enum):
68
68
  STORY = 7
69
69
 
70
70
 
71
- class nwTrinary(Enum):
72
-
73
- NEGATIVE = -1
74
- NEUTRAL = 0
75
- POSITIVE = 1
76
-
77
-
78
71
  class nwChange(Enum):
79
72
 
80
73
  CREATE = 0
@@ -90,12 +90,13 @@ class NovelSelector(QComboBox):
90
90
  @pyqtSlot()
91
91
  def refreshNovelList(self) -> None:
92
92
  """Rebuild the list of novel items."""
93
+ cHandle = self.currentData()
94
+
93
95
  self._blockSignal = True
94
96
  self._firstHandle = None
95
97
  self.clear()
96
98
 
97
99
  icon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])
98
- handle = self.currentData()
99
100
  for tHandle, nwItem in SHARED.project.tree.iterRoots(nwItemClass.NOVEL):
100
101
  if self._listFormat:
101
102
  name = self._listFormat.format(nwItem.itemName)
@@ -110,7 +111,7 @@ class NovelSelector(QComboBox):
110
111
  self.insertSeparator(self.count())
111
112
  self.addItem(icon, self.tr("All Novel Folders"), "")
112
113
 
113
- self.setHandle(handle)
114
+ self.setHandle(cHandle)
114
115
  self.setEnabled(self.count() > 1)
115
116
  self._blockSignal = False
116
117
 
@@ -29,7 +29,6 @@ from PyQt5.QtGui import QColor, QPainter, QPaintEvent
29
29
  from PyQt5.QtWidgets import QAbstractButton, QWidget
30
30
 
31
31
  from novelwriter import CONFIG
32
- from novelwriter.enum import nwTrinary
33
32
  from novelwriter.types import QtBlack, QtPaintAntiAlias
34
33
 
35
34
  logger = logging.getLogger(__name__)
@@ -47,14 +46,14 @@ class StatusLED(QAbstractButton):
47
46
  self._postitve = QtBlack
48
47
  self._negative = QtBlack
49
48
  self._color = QtBlack
50
- self._state = nwTrinary.NEUTRAL
49
+ self._state = None
51
50
  self._bPx = CONFIG.pxInt(1)
52
51
  self.setFixedWidth(sW)
53
52
  self.setFixedHeight(sH)
54
53
  return
55
54
 
56
55
  @property
57
- def state(self) -> nwTrinary:
56
+ def state(self) -> bool | None:
58
57
  """The current state of the LED."""
59
58
  return self._state
60
59
 
@@ -66,11 +65,11 @@ class StatusLED(QAbstractButton):
66
65
  self.setState(self._state)
67
66
  return
68
67
 
69
- def setState(self, state: nwTrinary) -> None:
68
+ def setState(self, state: bool | None) -> None:
70
69
  """Set the colour state."""
71
- if state == nwTrinary.POSITIVE:
70
+ if state is True:
72
71
  self._color = self._postitve
73
- elif state == nwTrinary.NEGATIVE:
72
+ elif state is False:
74
73
  self._color = self._negative
75
74
  else:
76
75
  self._color = self._neutral
@@ -104,17 +104,18 @@ class BlockTyp(IntEnum):
104
104
 
105
105
  EMPTY = 1 # Empty line (new paragraph)
106
106
  TITLE = 2 # Title
107
- HEAD1 = 3 # Heading 1
108
- HEAD2 = 4 # Heading 2
109
- HEAD3 = 5 # Heading 3
110
- HEAD4 = 6 # Heading 4
111
- TEXT = 7 # Text line
112
- SEP = 8 # Scene separator
113
- SKIP = 9 # Paragraph break
114
- SUMMARY = 10 # Synopsis/short comment
115
- NOTE = 11 # Note
116
- COMMENT = 12 # Comment
117
- KEYWORD = 13 # Tag/reference keywords
107
+ PART = 3 # Partition
108
+ HEAD1 = 4 # Heading 1 or Chapter
109
+ HEAD2 = 5 # Heading 2 or Scene
110
+ HEAD3 = 6 # Heading 3 or Section
111
+ HEAD4 = 7 # Heading 4
112
+ TEXT = 8 # Text line
113
+ SEP = 9 # Scene separator
114
+ SKIP = 10 # Paragraph break
115
+ SUMMARY = 11 # Synopsis/short comment
116
+ NOTE = 12 # Note
117
+ COMMENT = 13 # Comment
118
+ KEYWORD = 14 # Tag/reference keywords
118
119
 
119
120
 
120
121
  class BlockFmt(Flag):
@@ -271,7 +271,7 @@ class ToDocX(Tokenizer):
271
271
  if tType == BlockTyp.TEXT:
272
272
  self._processFragments(par, S_NORM, tText, tFormat)
273
273
 
274
- elif tType == BlockTyp.TITLE:
274
+ elif tType in (BlockTyp.TITLE, BlockTyp.PART):
275
275
  self._processFragments(par, S_TITLE, tText, tFormat)
276
276
 
277
277
  elif tType == BlockTyp.HEAD1:
@@ -30,7 +30,7 @@ from pathlib import Path
30
30
  from time import time
31
31
 
32
32
  from novelwriter.common import formatTimeStamp
33
- from novelwriter.constants import nwHtmlUnicode
33
+ from novelwriter.constants import nwHtmlUnicode, nwStyles
34
34
  from novelwriter.core.project import NWProject
35
35
  from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape
36
36
  from novelwriter.formats.tokenizer import Tokenizer
@@ -130,20 +130,6 @@ class ToHtml(Tokenizer):
130
130
 
131
131
  def doConvert(self) -> None:
132
132
  """Convert the list of text tokens into an HTML document."""
133
- if self._isNovel:
134
- # For story files, we bump the titles one level up
135
- h1Cl = " class='title'"
136
- h1 = "h1"
137
- h2 = "h1"
138
- h3 = "h2"
139
- h4 = "h3"
140
- else:
141
- h1Cl = ""
142
- h1 = "h1"
143
- h2 = "h2"
144
- h3 = "h3"
145
- h4 = "h4"
146
-
147
133
  lines = []
148
134
  for tType, tMeta, tText, tFmt, tStyle in self._blocks:
149
135
 
@@ -213,25 +199,25 @@ class ToHtml(Tokenizer):
213
199
  if tType == BlockTyp.TEXT:
214
200
  lines.append(f"<p{hStyle}>{self._formatText(tText, tFmt)}</p>\n")
215
201
 
216
- elif tType == BlockTyp.TITLE:
202
+ elif tType in (BlockTyp.TITLE, BlockTyp.PART):
217
203
  tHead = tText.replace("\n", "<br>")
218
204
  lines.append(f"<h1 class='title'{hStyle}>{aNm}{tHead}</h1>\n")
219
205
 
220
206
  elif tType == BlockTyp.HEAD1:
221
207
  tHead = tText.replace("\n", "<br>")
222
- lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}</{h1}>\n")
208
+ lines.append(f"<h1{hStyle}>{aNm}{tHead}</h1>\n")
223
209
 
224
210
  elif tType == BlockTyp.HEAD2:
225
211
  tHead = tText.replace("\n", "<br>")
226
- lines.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
212
+ lines.append(f"<h2{hStyle}>{aNm}{tHead}</h2>\n")
227
213
 
228
214
  elif tType == BlockTyp.HEAD3:
229
215
  tHead = tText.replace("\n", "<br>")
230
- lines.append(f"<{h3}{hStyle}>{aNm}{tHead}</{h3}>\n")
216
+ lines.append(f"<h3{hStyle}>{aNm}{tHead}</h3>\n")
231
217
 
232
218
  elif tType == BlockTyp.HEAD4:
233
219
  tHead = tText.replace("\n", "<br>")
234
- lines.append(f"<{h4}{hStyle}>{aNm}{tHead}</{h4}>\n")
220
+ lines.append(f"<h4{hStyle}>{aNm}{tHead}</h4>\n")
235
221
 
236
222
  elif tType == BlockTyp.SEP:
237
223
  lines.append(f"<p class='sep'{hStyle}>{tText}</p>\n")
@@ -310,9 +296,7 @@ class ToHtml(Tokenizer):
310
296
  "</style>\n"
311
297
  "</head>\n"
312
298
  "<body>\n"
313
- "<article>\n"
314
299
  "{body:s}\n"
315
- "</article>\n"
316
300
  "</body>\n"
317
301
  "</html>\n"
318
302
  ).format(
@@ -359,6 +343,12 @@ class ToHtml(Tokenizer):
359
343
  mtSP = self._marginSep[0]
360
344
  mbSP = self._marginSep[1]
361
345
 
346
+ fSz0 = nwStyles.H_SIZES[0]
347
+ fSz1 = nwStyles.H_SIZES[1]
348
+ fSz2 = nwStyles.H_SIZES[2]
349
+ fSz3 = nwStyles.H_SIZES[3]
350
+ fSz4 = nwStyles.H_SIZES[4]
351
+
362
352
  font = self._textFont
363
353
  fFam = font.family()
364
354
  fSz = font.pointSize()
@@ -379,12 +369,25 @@ class ToHtml(Tokenizer):
379
369
  styles.append(f"a {{color: {lColor};}}")
380
370
  styles.append(f"mark {{background: {mColor};}}")
381
371
  styles.append(f"h1, h2, h3, h4 {{color: {hColor}; page-break-after: avoid;}}")
382
- styles.append(f"h1 {{margin-top: {mtH1:.2f}em; margin-bottom: {mbH1:.2f}em;}}")
383
- styles.append(f"h2 {{margin-top: {mtH2:.2f}em; margin-bottom: {mbH2:.2f}em;}}")
384
- styles.append(f"h3 {{margin-top: {mtH3:.2f}em; margin-bottom: {mbH3:.2f}em;}}")
385
- styles.append(f"h4 {{margin-top: {mtH4:.2f}em; margin-bottom: {mbH4:.2f}em;}}")
386
372
  styles.append(
387
- f".title {{font-size: 2.5em; margin-top: {mtH0:.2f}em; margin-bottom: {mbH0:.2f}em;}}"
373
+ f"h1 {{font-size: {fSz1:.2f}em; "
374
+ f"margin-top: {mtH1:.2f}em; margin-bottom: {mbH1:.2f}em;}}"
375
+ )
376
+ styles.append(
377
+ f"h2 {{font-size: {fSz2:.2f}em; "
378
+ f"margin-top: {mtH2:.2f}em; margin-bottom: {mbH2:.2f}em;}}"
379
+ )
380
+ styles.append(
381
+ f"h3 {{font-size: {fSz3:.2f}em; "
382
+ f"margin-top: {mtH3:.2f}em; margin-bottom: {mbH3:.2f}em;}}"
383
+ )
384
+ styles.append(
385
+ f"h4 {{font-size: {fSz4:.2f}em; "
386
+ f"margin-top: {mtH4:.2f}em; margin-bottom: {mbH4:.2f}em;}}"
387
+ )
388
+ styles.append(
389
+ f".title {{font-size: {fSz0:.2f}em; "
390
+ f"margin-top: {mtH0:.2f}em; margin-bottom: {mbH0:.2f}em;}}"
388
391
  )
389
392
  styles.append(
390
393
  f".sep {{text-align: center; margin-top: {mtSP:.2f}em; margin-bottom: {mbSP:.2f}em;}}"
@@ -68,11 +68,11 @@ COMMENT_STYLE = {
68
68
  nwComment.COMMENT: ComStyle(),
69
69
  nwComment.STORY: ComStyle("", "modifier", "note"),
70
70
  }
71
- HEADINGS = [BlockTyp.TITLE, BlockTyp.HEAD1, BlockTyp.HEAD2, BlockTyp.HEAD3, BlockTyp.HEAD4]
72
- SKIP_INDENT = [
73
- BlockTyp.TITLE, BlockTyp.HEAD1, BlockTyp.HEAD2, BlockTyp.HEAD2, BlockTyp.HEAD3,
74
- BlockTyp.HEAD4, BlockTyp.SEP, BlockTyp.SKIP,
71
+ HEADINGS = [
72
+ BlockTyp.TITLE, BlockTyp.PART, BlockTyp.HEAD1,
73
+ BlockTyp.HEAD2, BlockTyp.HEAD3, BlockTyp.HEAD4,
75
74
  ]
75
+ SKIP_INDENT = HEADINGS + [BlockTyp.SEP, BlockTyp.SKIP]
76
76
  B_EMPTY: T_Block = (BlockTyp.EMPTY, "", "", [], BlockFmt.NONE)
77
77
 
78
78
 
@@ -654,6 +654,7 @@ class Tokenizer(ABC):
654
654
  if not (isPlain or isNovel and sHide):
655
655
  tStyle |= self._titleStyle
656
656
  if isNovel:
657
+ tType = BlockTyp.PART if isPlain else BlockTyp.TITLE
657
658
  if sHide:
658
659
  tText = ""
659
660
  tType = BlockTyp.EMPTY
@@ -687,6 +688,7 @@ class Tokenizer(ABC):
687
688
  sHide = self._hideChapter if isPlain else self._hideUnNum
688
689
  tFormat = self._fmtChapter if isPlain else self._fmtUnNum
689
690
  if isNovel:
691
+ tType = BlockTyp.HEAD1 # Promote
690
692
  if isPlain:
691
693
  self._hFormatter.incChapter()
692
694
  if sHide:
@@ -721,6 +723,7 @@ class Tokenizer(ABC):
721
723
  sHide = self._hideScene if isPlain else self._hideHScene
722
724
  tFormat = self._fmtScene if isPlain else self._fmtHScene
723
725
  if isNovel:
726
+ tType = BlockTyp.HEAD2 # Promote
724
727
  self._hFormatter.incScene()
725
728
  if sHide:
726
729
  tText = ""
@@ -752,6 +755,7 @@ class Tokenizer(ABC):
752
755
  tText = aLine[5:].strip()
753
756
  tType = BlockTyp.HEAD4
754
757
  if isNovel:
758
+ tType = BlockTyp.HEAD3 # Promote
755
759
  if self._hideSection:
756
760
  tText = ""
757
761
  tType = BlockTyp.EMPTY
@@ -927,12 +931,14 @@ class Tokenizer(ABC):
927
931
  for tType, tKey, tText, _, _ in self._blocks:
928
932
  if tType == BlockTyp.TITLE:
929
933
  prefix = "TT"
934
+ elif tType == BlockTyp.PART:
935
+ prefix = "PT"
930
936
  elif tType == BlockTyp.HEAD1:
931
- prefix = "PT" if isNovel else "H1"
937
+ prefix = "CH" if isNovel else "H1"
932
938
  elif tType == BlockTyp.HEAD2:
933
- prefix = "CH" if isNovel else "H2"
934
- elif tType == BlockTyp.HEAD3:
935
- prefix = "SC" if isNovel else "H3"
939
+ prefix = "SC" if isNovel else "H2"
940
+ elif tType == BlockTyp.HEAD3 and not isNovel:
941
+ prefix = "H3"
936
942
  else:
937
943
  continue
938
944
 
@@ -113,9 +113,10 @@ class ToMarkdown(Tokenizer):
113
113
  tTemp = self._formatText(tText, tFormat, mTags).replace("\n", " \n")
114
114
  lines.append(f"{tTemp}\n\n")
115
115
 
116
- elif tType == BlockTyp.TITLE:
116
+ elif tType in (BlockTyp.TITLE, BlockTyp.PART):
117
117
  tHead = tText.replace("\n", " - ")
118
- lines.append(f"# {tHead}\n\n")
118
+ lines.append(f"{tHead}\n")
119
+ lines.append("="*len(tHead) + "\n\n")
119
120
 
120
121
  elif tType == BlockTyp.HEAD1:
121
122
  tHead = tText.replace("\n", " - ")
@@ -363,7 +363,7 @@ class ToOdt(Tokenizer):
363
363
  else:
364
364
  self._addTextPar(xText, S_TEXT, oStyle, tText, tFmt=tFormat)
365
365
 
366
- elif tType == BlockTyp.TITLE:
366
+ elif tType in (BlockTyp.TITLE, BlockTyp.PART):
367
367
  # Title must be text:p
368
368
  self._addTextPar(xText, S_TITLE, oStyle, tText, isHead=False)
369
369
 
@@ -157,6 +157,7 @@ class ToQTextDocument(Tokenizer):
157
157
 
158
158
  self._mHead = {
159
159
  BlockTyp.TITLE: (fPx * self._marginTitle[0], fPx * self._marginTitle[1]),
160
+ BlockTyp.PART: (fPx * self._marginTitle[0], fPx * self._marginTitle[1]),
160
161
  BlockTyp.HEAD1: (fPx * self._marginHead1[0], fPx * self._marginHead1[1]),
161
162
  BlockTyp.HEAD2: (fPx * self._marginHead2[0], fPx * self._marginHead2[1]),
162
163
  BlockTyp.HEAD3: (fPx * self._marginHead3[0], fPx * self._marginHead3[1]),
@@ -166,6 +167,7 @@ class ToQTextDocument(Tokenizer):
166
167
  hScale = self._scaleHeads
167
168
  self._sHead = {
168
169
  BlockTyp.TITLE: (nwStyles.H_SIZES.get(0, 1.0) * fPt) if hScale else fPt,
170
+ BlockTyp.PART: (nwStyles.H_SIZES.get(0, 1.0) * fPt) if hScale else fPt,
169
171
  BlockTyp.HEAD1: (nwStyles.H_SIZES.get(1, 1.0) * fPt) if hScale else fPt,
170
172
  BlockTyp.HEAD2: (nwStyles.H_SIZES.get(2, 1.0) * fPt) if hScale else fPt,
171
173
  BlockTyp.HEAD3: (nwStyles.H_SIZES.get(3, 1.0) * fPt) if hScale else fPt,
@@ -34,7 +34,7 @@ from __future__ import annotations
34
34
  import bisect
35
35
  import logging
36
36
 
37
- from enum import Enum
37
+ from enum import Enum, IntFlag
38
38
  from time import time
39
39
 
40
40
  from PyQt5.QtCore import (
@@ -57,7 +57,7 @@ from novelwriter.constants import nwConst, nwKeyWords, nwShortcode, nwUnicode
57
57
  from novelwriter.core.document import NWDocument
58
58
  from novelwriter.enum import (
59
59
  nwChange, nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass,
60
- nwItemType, nwTrinary
60
+ nwItemType
61
61
  )
62
62
  from novelwriter.extensions.configlayout import NColourLabel
63
63
  from novelwriter.extensions.eventfilters import WheelEventFilter
@@ -84,6 +84,13 @@ class _SelectAction(Enum):
84
84
  MOVE_AFTER = 3
85
85
 
86
86
 
87
+ class _TagAction(IntFlag):
88
+
89
+ NONE = 0b00
90
+ FOLLOW = 0b01
91
+ CREATE = 0b10
92
+
93
+
87
94
  class GuiDocEditor(QPlainTextEdit):
88
95
  """Gui Widget: Main Document Editor"""
89
96
 
@@ -1104,10 +1111,10 @@ class GuiDocEditor(QPlainTextEdit):
1104
1111
  else:
1105
1112
  self._completer.setVisible(False)
1106
1113
 
1107
- if self._doReplace and added == 1:
1108
- cursor = self.textCursor()
1109
- if self._autoReplace.process(text, cursor):
1110
- self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
1114
+ if self._doReplace and added == 1:
1115
+ cursor = self.textCursor()
1116
+ if self._autoReplace.process(text, cursor):
1117
+ self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
1111
1118
 
1112
1119
  return
1113
1120
 
@@ -1158,11 +1165,11 @@ class GuiDocEditor(QPlainTextEdit):
1158
1165
 
1159
1166
  # Follow
1160
1167
  status = self._processTag(cursor=pCursor, follow=False)
1161
- if status == nwTrinary.POSITIVE:
1168
+ if status & _TagAction.FOLLOW:
1162
1169
  action = ctxMenu.addAction(self.tr("Follow Tag"))
1163
1170
  action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, follow=True))
1164
1171
  ctxMenu.addSeparator()
1165
- elif status == nwTrinary.NEGATIVE:
1172
+ elif status & _TagAction.CREATE:
1166
1173
  action = ctxMenu.addAction(self.tr("Create Note for Tag"))
1167
1174
  action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, create=True))
1168
1175
  ctxMenu.addSeparator()
@@ -1217,7 +1224,7 @@ class GuiDocEditor(QPlainTextEdit):
1217
1224
 
1218
1225
  # Execute the context menu
1219
1226
  ctxMenu.exec(self.viewport().mapToGlobal(pos))
1220
- ctxMenu.deleteLater()
1227
+ ctxMenu.setParent(None)
1221
1228
 
1222
1229
  return
1223
1230
 
@@ -1925,8 +1932,9 @@ class GuiDocEditor(QPlainTextEdit):
1925
1932
  self._qDocument.syntaxHighlighter.rehighlightBlock(block)
1926
1933
  return
1927
1934
 
1928
- def _processTag(self, cursor: QTextCursor | None = None,
1929
- follow: bool = True, create: bool = False) -> nwTrinary:
1935
+ def _processTag(
1936
+ self, cursor: QTextCursor | None = None, follow: bool = True, create: bool = False
1937
+ ) -> _TagAction:
1930
1938
  """Activated by Ctrl+Enter. Checks that we're in a block
1931
1939
  starting with '@'. We then find the tag under the cursor and
1932
1940
  check that it is not the tag itself. If all this is fine, we
@@ -1936,19 +1944,22 @@ class GuiDocEditor(QPlainTextEdit):
1936
1944
  if cursor is None:
1937
1945
  cursor = self.textCursor()
1938
1946
 
1947
+ status = _TagAction.NONE
1939
1948
  block = cursor.block()
1940
1949
  text = block.text()
1941
1950
  if len(text) == 0:
1942
- return nwTrinary.NEUTRAL
1951
+ return status
1943
1952
 
1944
1953
  if text.startswith("@") and self._docHandle:
1945
1954
 
1946
1955
  isGood, tBits, tPos = SHARED.project.index.scanThis(text)
1947
1956
  if (
1948
- not isGood or not tBits or tBits[0] == nwKeyWords.TAG_KEY
1949
- or tBits[0] not in nwKeyWords.VALID_KEYS
1957
+ not isGood
1958
+ or not tBits
1959
+ or (key := tBits[0]) == nwKeyWords.TAG_KEY
1960
+ or key not in nwKeyWords.VALID_KEYS
1950
1961
  ):
1951
- return nwTrinary.NEUTRAL
1962
+ return status
1952
1963
 
1953
1964
  tag = ""
1954
1965
  exist = False
@@ -1965,7 +1976,14 @@ class GuiDocEditor(QPlainTextEdit):
1965
1976
 
1966
1977
  if not tag or tag.startswith("@"):
1967
1978
  # The keyword cannot be looked up, so we ignore that
1968
- return nwTrinary.NEUTRAL
1979
+ return status
1980
+
1981
+ if not exist and key in nwKeyWords.CAN_CREATE:
1982
+ # Must only be set if we have a tag selected
1983
+ status |= _TagAction.CREATE
1984
+
1985
+ if exist:
1986
+ status |= _TagAction.FOLLOW
1969
1987
 
1970
1988
  if follow and exist:
1971
1989
  logger.debug("Attempting to follow tag '%s'", tag)
@@ -1977,9 +1995,7 @@ class GuiDocEditor(QPlainTextEdit):
1977
1995
  itemClass = nwKeyWords.KEY_CLASS.get(tBits[0], nwItemClass.NO_CLASS)
1978
1996
  self.requestNewNoteCreation.emit(tag, itemClass)
1979
1997
 
1980
- return nwTrinary.POSITIVE if exist else nwTrinary.NEGATIVE
1981
-
1982
- return nwTrinary.NEUTRAL
1998
+ return status
1983
1999
 
1984
2000
  def _emitRenameItem(self, block: QTextBlock) -> None:
1985
2001
  """Emit a signal to request an item be renamed."""
@@ -424,7 +424,7 @@ class GuiDocViewer(QTextBrowser):
424
424
 
425
425
  # Open the context menu
426
426
  ctxMenu.exec(self.viewport().mapToGlobal(point))
427
- ctxMenu.deleteLater()
427
+ ctxMenu.setParent(None)
428
428
 
429
429
  return
430
430
 
@@ -93,6 +93,7 @@ class GuiItemDetails(QWidget):
93
93
  self.statusData = QLabel("", self)
94
94
  self.statusData.setFont(fntValue)
95
95
  self.statusData.setAlignment(QtAlignLeft)
96
+ self.statusData.setWordWrap(True)
96
97
 
97
98
  # Class
98
99
  self.className = QLabel(self.tr("Class"), self)
@@ -955,23 +955,18 @@ class GuiProjectTree(QTreeView):
955
955
  if model := self._getModel():
956
956
  if point is None:
957
957
  point = self.visualRect(self.currentIndex()).center()
958
-
959
- if (
960
- point is not None
961
- and (node := self._getNode(self.currentIndex()))
962
- and (indices := self._selectedRows())
963
- ):
964
- ctxMenu = _TreeContextMenu(self, model, node, indices)
965
- if node is SHARED.project.tree.trash:
966
- ctxMenu.buildTrashMenu()
967
- elif len(indices) > 1:
968
- ctxMenu.buildMultiSelectMenu()
969
- else:
970
- ctxMenu.buildSingleSelectMenu()
971
-
972
- ctxMenu.exec(self.viewport().mapToGlobal(point))
973
- ctxMenu.deleteLater()
974
-
958
+ if point is not None:
959
+ index = self.indexAt(point)
960
+ if (node := self._getNode(index)) and (indices := self._selectedRows()):
961
+ ctxMenu = _TreeContextMenu(self, model, node, indices)
962
+ if node is SHARED.project.tree.trash:
963
+ ctxMenu.buildTrashMenu()
964
+ elif len(indices) > 1:
965
+ ctxMenu.buildMultiSelectMenu()
966
+ else:
967
+ ctxMenu.buildSingleSelectMenu()
968
+ ctxMenu.exec(self.viewport().mapToGlobal(point))
969
+ ctxMenu.setParent(None)
975
970
  return
976
971
 
977
972
  ##
@@ -1020,7 +1015,9 @@ class GuiProjectTree(QTreeView):
1020
1015
  def _clearSelection(self) -> None:
1021
1016
  """Clear the currently selected items."""
1022
1017
  self.clearSelection()
1023
- self.selectionModel().clearCurrentIndex()
1018
+ if model := self.selectionModel():
1019
+ # Selection model can be None (#2173)
1020
+ model.clearCurrentIndex()
1024
1021
  return
1025
1022
 
1026
1023
  def _selectedRows(self) -> list[QModelIndex]:
@@ -34,7 +34,6 @@ from PyQt5.QtWidgets import QApplication, QLabel, QStatusBar, QWidget
34
34
  from novelwriter import CONFIG, SHARED
35
35
  from novelwriter.common import formatTime
36
36
  from novelwriter.constants import nwConst
37
- from novelwriter.enum import nwTrinary
38
37
  from novelwriter.extensions.modified import NClickableLabel
39
38
  from novelwriter.extensions.statusled import StatusLED
40
39
 
@@ -121,8 +120,8 @@ class GuiMainStatus(QStatusBar):
121
120
  self.setRefTime(-1.0)
122
121
  self.setLanguage(*SHARED.spelling.describeDict())
123
122
  self.setProjectStats(0, 0)
124
- self.setProjectStatus(nwTrinary.NEUTRAL)
125
- self.setDocumentStatus(nwTrinary.NEUTRAL)
123
+ self.setProjectStatus(None)
124
+ self.setDocumentStatus(None)
126
125
  self.updateTime()
127
126
  return
128
127
 
@@ -152,12 +151,12 @@ class GuiMainStatus(QStatusBar):
152
151
  self._refTime = refTime
153
152
  return
154
153
 
155
- def setProjectStatus(self, state: nwTrinary) -> None:
154
+ def setProjectStatus(self, state: bool | None) -> None:
156
155
  """Set the project status colour icon."""
157
156
  self.projIcon.setState(state)
158
157
  return
159
158
 
160
- def setDocumentStatus(self, state: nwTrinary) -> None:
159
+ def setDocumentStatus(self, state: bool | None) -> None:
161
160
  """Set the document status colour icon."""
162
161
  self.docIcon.setState(state)
163
162
  return
@@ -220,13 +219,13 @@ class GuiMainStatus(QStatusBar):
220
219
  @pyqtSlot(bool)
221
220
  def updateProjectStatus(self, status: bool) -> None:
222
221
  """Update the project status."""
223
- self.setProjectStatus(nwTrinary.NEGATIVE if status else nwTrinary.POSITIVE)
222
+ self.setProjectStatus(not status)
224
223
  return
225
224
 
226
225
  @pyqtSlot(bool)
227
226
  def updateDocumentStatus(self, status: bool) -> None:
228
227
  """Update the document status."""
229
- self.setDocumentStatus(nwTrinary.NEGATIVE if status else nwTrinary.POSITIVE)
228
+ self.setDocumentStatus(not status)
230
229
  return
231
230
 
232
231
  ##
novelwriter/guimain.py CHANGED
@@ -852,7 +852,7 @@ class GuiMain(QMainWindow):
852
852
 
853
853
  def closeMain(self) -> bool:
854
854
  """Save everything, and close novelWriter."""
855
- if SHARED.hasProject and not SHARED.question("%s<br>%s" % (
855
+ if SHARED.hasProject and CONFIG.askBeforeExit and not SHARED.question("%s<br>%s" % (
856
856
  self.tr("Do you want to exit novelWriter?"),
857
857
  self.tr("Changes are saved automatically.")
858
858
  )):
@@ -65,12 +65,12 @@ class GuiDictionaries(NNonBlockingDialog):
65
65
  self.setMinimumHeight(CONFIG.pxInt(300))
66
66
 
67
67
  # Hunspell Dictionaries
68
- foUrl = "https://www.freeoffice.com/en/download/dictionaries"
69
68
  loUrl = "https://extensions.libreoffice.org"
69
+ ooUrl = "https://extensions.openoffice.org"
70
70
  self.huInfo = QLabel("<br>".join([
71
71
  self.tr("Download a dictionary from one of the links, and add it below."),
72
- f"&nbsp;\u203a <a href='{foUrl}'>{foUrl}</a>",
73
72
  f"&nbsp;\u203a <a href='{loUrl}'>{loUrl}</a>",
73
+ f"&nbsp;\u203a <a href='{ooUrl}'>{ooUrl}</a>",
74
74
  ]), self)
75
75
  self.huInfo.setOpenExternalLinks(True)
76
76
  self.huInfo.setWordWrap(True)