novelWriter 2.4rc1__py3-none-any.whl → 2.4.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 (48) hide show
  1. {novelWriter-2.4rc1.dist-info → novelWriter-2.4.1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.4rc1.dist-info → novelWriter-2.4.1.dist-info}/RECORD +48 -48
  3. novelwriter/__init__.py +13 -6
  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_pt_BR.qm +0 -0
  13. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  14. novelwriter/assets/manual.pdf +0 -0
  15. novelwriter/assets/sample.zip +0 -0
  16. novelwriter/common.py +5 -2
  17. novelwriter/config.py +4 -0
  18. novelwriter/core/buildsettings.py +7 -7
  19. novelwriter/core/docbuild.py +2 -2
  20. novelwriter/core/projectxml.py +1 -1
  21. novelwriter/core/spellcheck.py +3 -3
  22. novelwriter/core/tokenizer.py +3 -3
  23. novelwriter/core/toodt.py +1 -1
  24. novelwriter/dialogs/preferences.py +1 -1
  25. novelwriter/dialogs/projectsettings.py +2 -2
  26. novelwriter/dialogs/wordlist.py +2 -2
  27. novelwriter/error.py +1 -1
  28. novelwriter/gui/doceditor.py +27 -25
  29. novelwriter/gui/dochighlight.py +22 -5
  30. novelwriter/gui/docviewer.py +3 -3
  31. novelwriter/gui/mainmenu.py +2 -2
  32. novelwriter/gui/noveltree.py +1 -1
  33. novelwriter/gui/outline.py +9 -9
  34. novelwriter/gui/projtree.py +1 -1
  35. novelwriter/gui/search.py +2 -2
  36. novelwriter/gui/theme.py +15 -5
  37. novelwriter/guimain.py +2 -2
  38. novelwriter/shared.py +16 -4
  39. novelwriter/text/counting.py +1 -0
  40. novelwriter/tools/dictionaries.py +2 -2
  41. novelwriter/tools/manusbuild.py +12 -11
  42. novelwriter/tools/manuscript.py +48 -43
  43. novelwriter/tools/manussettings.py +27 -29
  44. novelwriter/tools/welcome.py +4 -5
  45. {novelWriter-2.4rc1.dist-info → novelWriter-2.4.1.dist-info}/LICENSE.md +0 -0
  46. {novelWriter-2.4rc1.dist-info → novelWriter-2.4.1.dist-info}/WHEEL +0 -0
  47. {novelWriter-2.4rc1.dist-info → novelWriter-2.4.1.dist-info}/entry_points.txt +0 -0
  48. {novelWriter-2.4rc1.dist-info → novelWriter-2.4.1.dist-info}/top_level.txt +0 -0
@@ -39,8 +39,8 @@ from time import time
39
39
  from typing import TYPE_CHECKING
40
40
 
41
41
  from PyQt5.QtCore import (
42
- pyqtSignal, pyqtSlot, QObject, QPoint, QRegExp, QRegularExpression,
43
- QRunnable, Qt, QTimer
42
+ QObject, QPoint, QRegExp, QRegularExpression, QRunnable, Qt, QTimer,
43
+ pyqtSignal, pyqtSlot
44
44
  )
45
45
  from PyQt5.QtGui import (
46
46
  QColor, QCursor, QFont, QKeyEvent, QKeySequence, QMouseEvent, QPalette,
@@ -65,7 +65,7 @@ from novelwriter.text.counting import standardCounter
65
65
  from novelwriter.tools.lipsum import GuiLipsum
66
66
  from novelwriter.types import (
67
67
  QtAlignCenterTop, QtAlignJustify, QtAlignLeft, QtAlignLeftTop,
68
- QtAlignRight, QtKeepAnchor, QtModCtrl, QtMouseLeft, QtModeNone, QtModShift,
68
+ QtAlignRight, QtKeepAnchor, QtModCtrl, QtModeNone, QtModShift, QtMouseLeft,
69
69
  QtMoveAnchor, QtMoveLeft, QtMoveRight
70
70
  )
71
71
 
@@ -334,7 +334,7 @@ class GuiDocEditor(QPlainTextEdit):
334
334
  font = QFont()
335
335
  font.setFamily(CONFIG.textFont)
336
336
  font.setPointSize(CONFIG.textSize)
337
- self.setFont(font)
337
+ self._qDocument.setDefaultFont(font)
338
338
 
339
339
  # Set default text margins
340
340
  # Due to cursor visibility, a part of the margin must be
@@ -381,7 +381,7 @@ class GuiDocEditor(QPlainTextEdit):
381
381
 
382
382
  return
383
383
 
384
- def loadText(self, tHandle: str, tLine=None) -> bool:
384
+ def loadText(self, tHandle: str, tLine: int | None = None) -> bool:
385
385
  """Load text from a document into the editor. If we have an I/O
386
386
  error, we must handle this and clear the editor so that we don't
387
387
  risk overwriting the file if it exists. This can for instance
@@ -497,6 +497,7 @@ class GuiDocEditor(QPlainTextEdit):
497
497
  return False
498
498
 
499
499
  self.setDocumentChanged(False)
500
+ self.docTextChanged.emit(self._docHandle, self._lastEdit)
500
501
 
501
502
  oldHeader = self._nwItem.mainHeading
502
503
  oldCount = SHARED.project.index.getHandleHeaderCount(tHandle)
@@ -1080,7 +1081,7 @@ class GuiDocEditor(QPlainTextEdit):
1080
1081
  return
1081
1082
 
1082
1083
  @pyqtSlot()
1083
- def _cursorMoved(self):
1084
+ def _cursorMoved(self) -> None:
1084
1085
  """Triggered when the cursor moved in the editor."""
1085
1086
  self.docFooter.updateLineCount(self.textCursor())
1086
1087
  return
@@ -1470,7 +1471,7 @@ class GuiDocEditor(QPlainTextEdit):
1470
1471
  # Internal Functions : Text Manipulation
1471
1472
  ##
1472
1473
 
1473
- def _toggleFormat(self, fLen: int, fChar: str) -> bool:
1474
+ def _toggleFormat(self, fLen: int, fChar: str) -> None:
1474
1475
  """Toggle the formatting of a specific type for a piece of text.
1475
1476
  If more than one block is selected, the formatting is applied to
1476
1477
  the first block.
@@ -1488,12 +1489,12 @@ class GuiDocEditor(QPlainTextEdit):
1488
1489
 
1489
1490
  posS = cursor.selectionStart()
1490
1491
  posE = cursor.selectionEnd()
1491
- if self._qDocument.characterAt(posO - 1) == fChar:
1492
+ if posS == posE and self._qDocument.characterAt(posO - 1) == fChar:
1492
1493
  logger.warning("Format repetition, cancelling action")
1493
1494
  cursor.clearSelection()
1494
1495
  cursor.setPosition(posO)
1495
1496
  self.setTextCursor(cursor)
1496
- return False
1497
+ return
1497
1498
 
1498
1499
  blockS = self._qDocument.findBlock(posS)
1499
1500
  blockE = self._qDocument.findBlock(posE)
@@ -1519,7 +1520,6 @@ class GuiDocEditor(QPlainTextEdit):
1519
1520
  break
1520
1521
 
1521
1522
  if fLen == min(numA, numB):
1522
- cursor.clearSelection()
1523
1523
  cursor.beginEditBlock()
1524
1524
  cursor.setPosition(posS)
1525
1525
  for i in range(fLen):
@@ -1528,17 +1528,19 @@ class GuiDocEditor(QPlainTextEdit):
1528
1528
  for i in range(fLen):
1529
1529
  cursor.deletePreviousChar()
1530
1530
  cursor.endEditBlock()
1531
- cursor.clearSelection()
1532
- cursor.setPosition(posO - fLen)
1533
- self.setTextCursor(cursor)
1531
+
1532
+ if select != _SelectAction.KEEP_SELECTION:
1533
+ cursor.clearSelection()
1534
+ cursor.setPosition(posO - fLen)
1535
+ self.setTextCursor(cursor)
1534
1536
 
1535
1537
  else:
1536
1538
  self._wrapSelection(fChar*fLen, pos=posO, select=select)
1537
1539
 
1538
- return True
1540
+ return
1539
1541
 
1540
1542
  def _wrapSelection(self, before: str, after: str | None = None, pos: int | None = None,
1541
- select: _SelectAction = _SelectAction.NO_DECISION) -> bool:
1543
+ select: _SelectAction = _SelectAction.NO_DECISION) -> None:
1542
1544
  """Wrap the selected text in whatever is in tBefore and tAfter.
1543
1545
  If there is no selection, the autoSelect setting decides the
1544
1546
  action. AutoSelect will select the word under the cursor before
@@ -1578,21 +1580,21 @@ class GuiDocEditor(QPlainTextEdit):
1578
1580
  if select == _SelectAction.MOVE_AFTER:
1579
1581
  cursor.setPosition(posE + len(before + after))
1580
1582
  elif select == _SelectAction.KEEP_SELECTION:
1581
- cursor.setPosition(posE + len(before), QtMoveAnchor)
1582
- cursor.setPosition(posS + len(before), QtKeepAnchor)
1583
+ cursor.setPosition(posS + len(before), QtMoveAnchor)
1584
+ cursor.setPosition(posE + len(before), QtKeepAnchor)
1583
1585
  elif select == _SelectAction.KEEP_POSITION:
1584
1586
  cursor.setPosition(posO + len(before))
1585
1587
 
1586
1588
  self.setTextCursor(cursor)
1587
1589
 
1588
- return True
1590
+ return
1589
1591
 
1590
- def _replaceQuotes(self, sQuote: str, oQuote: str, cQuote: str) -> bool:
1592
+ def _replaceQuotes(self, sQuote: str, oQuote: str, cQuote: str) -> None:
1591
1593
  """Replace all straight quotes in the selected text."""
1592
1594
  cursor = self.textCursor()
1593
1595
  if not cursor.hasSelection():
1594
1596
  SHARED.error(self.tr("Please select some text before calling replace quotes."))
1595
- return False
1597
+ return
1596
1598
 
1597
1599
  posS = cursor.selectionStart()
1598
1600
  posE = cursor.selectionEnd()
@@ -1632,7 +1634,7 @@ class GuiDocEditor(QPlainTextEdit):
1632
1634
 
1633
1635
  self._allowAutoReplace(True)
1634
1636
 
1635
- return True
1637
+ return
1636
1638
 
1637
1639
  def _processBlockFormat(
1638
1640
  self, action: nwDocAction, text: str, toggle: bool = True
@@ -2185,7 +2187,7 @@ class MetaCompleter(QMenu):
2185
2187
  # Internal Functions
2186
2188
  ##
2187
2189
 
2188
- def _emitComplete(self, pos: int, length: int, value: str):
2190
+ def _emitComplete(self, pos: int, length: int, value: str) -> None:
2189
2191
  """Emit the signal to indicate a selection has been made."""
2190
2192
  self.complete.emit(pos, length, value)
2191
2193
  return
@@ -2408,12 +2410,12 @@ class GuiDocEditSearch(QFrame):
2408
2410
 
2409
2411
  self.searchBox = QLineEdit(self)
2410
2412
  self.searchBox.setFont(self.boxFont)
2411
- self.searchBox.setPlaceholderText(self.tr("Search"))
2413
+ self.searchBox.setPlaceholderText(self.tr("Search for"))
2412
2414
  self.searchBox.returnPressed.connect(self._doSearch)
2413
2415
 
2414
2416
  self.replaceBox = QLineEdit(self)
2415
2417
  self.replaceBox.setFont(self.boxFont)
2416
- self.replaceBox.setPlaceholderText(self.tr("Replace"))
2418
+ self.replaceBox.setPlaceholderText(self.tr("Replace with"))
2417
2419
  self.replaceBox.returnPressed.connect(self._doReplace)
2418
2420
 
2419
2421
  self.searchOpt = QToolBar(self)
@@ -2966,7 +2968,7 @@ class GuiDocEditHeader(QWidget):
2966
2968
  # Events
2967
2969
  ##
2968
2970
 
2969
- def mousePressEvent(self, event: QMouseEvent):
2971
+ def mousePressEvent(self, event: QMouseEvent) -> None:
2970
2972
  """Capture a click on the title and ensure that the item is
2971
2973
  selected in the project tree.
2972
2974
  """
@@ -44,6 +44,10 @@ logger = logging.getLogger(__name__)
44
44
 
45
45
  SPELLRX = QRegularExpression(r"\b[^\s\-\+\/–—\[\]:]+\b")
46
46
  SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
47
+ SPELLSC = QRegularExpression(nwRegEx.FMT_SC)
48
+ SPELLSC.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
49
+ SPELLSV = QRegularExpression(nwRegEx.FMT_SV)
50
+ SPELLSV.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
47
51
 
48
52
  BLOCK_NONE = 0
49
53
  BLOCK_TEXT = 1
@@ -53,7 +57,8 @@ BLOCK_TITLE = 4
53
57
 
54
58
  class GuiDocHighlighter(QSyntaxHighlighter):
55
59
 
56
- __slots__ = ("_tItem", "_tHandle", "_spellCheck", "_spellErr", "_hRules", "_hStyles")
60
+ __slots__ = ("_tHandle", "_isInactive", "_spellCheck", "_spellErr",
61
+ "_hRules", "_hStyles", "_rxRules")
57
62
 
58
63
  def __init__(self, document: QTextDocument) -> None:
59
64
  super().__init__(document)
@@ -67,6 +72,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
67
72
 
68
73
  self._hRules: list[tuple[str, dict]] = []
69
74
  self._hStyles: dict[str, QTextCharFormat] = {}
75
+ self._rxRules: list[tuple[QRegularExpression, dict[str, QTextCharFormat]]] = []
70
76
 
71
77
  self.initHighlighter()
72
78
 
@@ -218,11 +224,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
218
224
  ))
219
225
 
220
226
  # Build a QRegExp for each highlight pattern
221
- self.rxRules = []
227
+ self._rxRules = []
222
228
  for regEx, regRules in self._hRules:
223
229
  hReg = QRegularExpression(regEx)
224
230
  hReg.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
225
- self.rxRules.append((hReg, regRules))
231
+ self._rxRules.append((hReg, regRules))
226
232
 
227
233
  return
228
234
 
@@ -327,7 +333,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
327
333
  self.setFormat(0, 3, self._hStyles["head2h"])
328
334
  self.setFormat(3, len(text), self._hStyles["header2"])
329
335
 
330
- elif text.startswith("###! "): # Hard Scene
336
+ elif text.startswith("###! "): # Alternative Scene
331
337
  self.setFormat(0, 4, self._hStyles["head3h"])
332
338
  self.setFormat(4, len(text), self._hStyles["header3"])
333
339
 
@@ -358,7 +364,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
358
364
 
359
365
  # Regular Text
360
366
  self.setCurrentBlockState(BLOCK_TEXT)
361
- for rX, xFmt in self.rxRules:
367
+ for rX, xFmt in self._rxRules:
362
368
  rxItt = rX.globalMatch(text, 0)
363
369
  while rxItt.hasNext():
364
370
  rxMatch = rxItt.next()
@@ -439,6 +445,17 @@ class TextBlockData(QTextBlockUserData):
439
445
  """Run the spell checker and cache the result, and return the
440
446
  list of spell check errors.
441
447
  """
448
+ if "[" in text:
449
+ # Strip shortcodes
450
+ for rX in [SPELLSC, SPELLSV]:
451
+ rxItt = rX.globalMatch(text, 0)
452
+ while rxItt.hasNext():
453
+ rxMatch = rxItt.next()
454
+ xPos = rxMatch.capturedStart(0)
455
+ xLen = rxMatch.capturedLength(0)
456
+ xEnd = rxMatch.capturedEnd(0)
457
+ text = text[:xPos] + " "*xLen + text[xEnd:]
458
+
442
459
  self._spellErrors = []
443
460
  rxSpell = SPELLRX.globalMatch(text.replace("_", " "), 0)
444
461
  while rxSpell.hasNext():
@@ -30,7 +30,7 @@ import logging
30
30
 
31
31
  from enum import Enum
32
32
 
33
- from PyQt5.QtCore import pyqtSignal, pyqtSlot, QPoint, Qt, QUrl
33
+ from PyQt5.QtCore import QPoint, Qt, QUrl, pyqtSignal, pyqtSlot
34
34
  from PyQt5.QtGui import (
35
35
  QCursor, QFont, QMouseEvent, QPalette, QResizeEvent, QTextCursor,
36
36
  QTextOption
@@ -44,7 +44,7 @@ from novelwriter import CONFIG, SHARED
44
44
  from novelwriter.common import cssCol
45
45
  from novelwriter.constants import nwHeaders, nwUnicode
46
46
  from novelwriter.core.tohtml import ToHtml
47
- from novelwriter.enum import nwItemType, nwDocAction, nwDocMode
47
+ from novelwriter.enum import nwDocAction, nwDocMode, nwItemType
48
48
  from novelwriter.error import logException
49
49
  from novelwriter.extensions.eventfilters import WheelEventFilter
50
50
  from novelwriter.extensions.modified import NIconToolButton
@@ -146,7 +146,7 @@ class GuiDocViewer(QTextBrowser):
146
146
  font = QFont()
147
147
  font.setFamily(CONFIG.textFont)
148
148
  font.setPointSize(CONFIG.textSize)
149
- self.setFont(font)
149
+ self.document().setDefaultFont(font)
150
150
 
151
151
  # Set the widget colours to match syntax theme
152
152
  mainPalette = self.palette()
@@ -736,8 +736,8 @@ class GuiMainMenu(QMenuBar):
736
736
  lambda: self.requestDocAction.emit(nwDocAction.BLOCK_UNN)
737
737
  )
738
738
 
739
- # Format > Hard Scene
740
- self.aFmtHardSc = self.fmtMenu.addAction(self.tr("Hard Scene"))
739
+ # Format > Alternative Scene
740
+ self.aFmtHardSc = self.fmtMenu.addAction(self.tr("Alternative Scene"))
741
741
  self.aFmtHardSc.triggered.connect(
742
742
  lambda: self.requestDocAction.emit(nwDocAction.BLOCK_HSC)
743
743
  )
@@ -344,7 +344,7 @@ class GuiNovelToolBar(QWidget):
344
344
  # Internal Functions
345
345
  ##
346
346
 
347
- def _addLastColAction(self, colType, actionLabel) -> None:
347
+ def _addLastColAction(self, colType: NovelTreeColumn, actionLabel: str) -> None:
348
348
  """Add a column selection entry to the last column menu."""
349
349
  aLast = self.mLastCol.addAction(actionLabel)
350
350
  aLast.setCheckable(True)
@@ -47,6 +47,7 @@ from novelwriter.enum import (
47
47
  from novelwriter.error import logException
48
48
  from novelwriter.common import checkInt, formatFileFilter, makeFileNameSafe
49
49
  from novelwriter.constants import nwHeaders, trConst, nwKeyWords, nwLabels
50
+ from novelwriter.extensions.configlayout import NColourLabel
50
51
  from novelwriter.extensions.novelselector import NovelSelector
51
52
  from novelwriter.types import (
52
53
  QtAlignLeftTop, QtAlignRight, QtAlignRightTop, QtDecoration, QtUserRole
@@ -190,7 +191,7 @@ class GuiOutlineView(QWidget):
190
191
  return
191
192
 
192
193
  @pyqtSlot(str)
193
- def _rootItemChanged(self, tHandle) -> None:
194
+ def _rootItemChanged(self, tHandle: str) -> None:
194
195
  """Handle root novel changed or needs to be refreshed."""
195
196
  self.outlineTree.refreshTree(rootHandle=(tHandle or None), overRide=True)
196
197
  return
@@ -209,19 +210,18 @@ class GuiOutlineToolBar(QToolBar):
209
210
 
210
211
  logger.debug("Create: GuiOutlineToolBar")
211
212
 
212
- iSz = SHARED.theme.baseIconSize
213
- mPx = CONFIG.pxInt(12)
214
-
215
213
  self.setMovable(False)
216
- self.setIconSize(iSz)
214
+ self.setIconSize(1.4*SHARED.theme.baseIconSize)
217
215
  self.setContentsMargins(0, 0, 0, 0)
218
216
 
219
217
  stretch = QWidget(self)
220
218
  stretch.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
221
219
 
222
220
  # Novel Selector
223
- self.novelLabel = QLabel(self.tr("Outline of"), self)
224
- self.novelLabel.setContentsMargins(0, 0, mPx, 0)
221
+ self.novelLabel = NColourLabel(
222
+ self.tr("Outline of"), parent=self, scale=NColourLabel.HEADER_SCALE, bold=True
223
+ )
224
+ self.novelLabel.setContentsMargins(0, 0, CONFIG.pxInt(12), 0)
225
225
 
226
226
  self.novelValue = NovelSelector(self)
227
227
  self.novelValue.setIncludeAll(True)
@@ -428,7 +428,7 @@ class GuiOutlineTree(QTreeWidget):
428
428
  ##
429
429
 
430
430
  @property
431
- def hiddenColumns(self):
431
+ def hiddenColumns(self) -> dict[nwOutline, bool]:
432
432
  return self._colHidden
433
433
 
434
434
  ##
@@ -586,7 +586,7 @@ class GuiOutlineTree(QTreeWidget):
586
586
  # Internal Functions
587
587
  ##
588
588
 
589
- def _loadHeaderState(self):
589
+ def _loadHeaderState(self) -> None:
590
590
  """Load the state of the main tree header, that is, column order
591
591
  and column width.
592
592
  """
@@ -453,7 +453,7 @@ class GuiProjectToolBar(QWidget):
453
453
 
454
454
  def _buildRootMenu(self) -> None:
455
455
  """Build the rood folder menu."""
456
- def addClass(itemClass):
456
+ def addClass(itemClass: nwItemClass) -> None:
457
457
  aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass]))
458
458
  aNew.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass]))
459
459
  aNew.triggered.connect(lambda: self.projTree.newTreeItem(nwItemType.ROOT, itemClass))
novelwriter/gui/search.py CHANGED
@@ -98,7 +98,7 @@ class GuiProjectSearch(QWidget):
98
98
 
99
99
  # Search Box
100
100
  self.searchText = QLineEdit(self)
101
- self.searchText.setPlaceholderText(self.tr("Search"))
101
+ self.searchText.setPlaceholderText(self.tr("Search for"))
102
102
  self.searchText.setClearButtonEnabled(True)
103
103
 
104
104
  self.searchAction = self.searchText.addAction(
@@ -161,7 +161,7 @@ class GuiProjectSearch(QWidget):
161
161
  colBase = cssCol(qPalette.base().color())
162
162
  colFocus = cssCol(qPalette.highlight().color())
163
163
 
164
- self.headerWidget.setStyleSheet(f"background: {colBase};")
164
+ self.headerWidget.setStyleSheet(f"QWidget {{background: {colBase};}}")
165
165
  self.headerWidget.setAutoFillBackground(True)
166
166
 
167
167
  self.setStyleSheet(
novelwriter/gui/theme.py CHANGED
@@ -74,9 +74,9 @@ class GuiTheme:
74
74
  self.isLightTheme = True
75
75
 
76
76
  # GUI
77
- self.statNone = QColor(120, 120, 120)
78
- self.statUnsaved = QColor(200, 15, 39)
79
- self.statSaved = QColor(2, 133, 37)
77
+ self.statNone = QColor(0, 0, 0)
78
+ self.statUnsaved = QColor(0, 0, 0)
79
+ self.statSaved = QColor(0, 0, 0)
80
80
  self.helpText = QColor(0, 0, 0)
81
81
 
82
82
  # Loaded Syntax Settings
@@ -227,6 +227,10 @@ class GuiTheme:
227
227
  logException()
228
228
  return False
229
229
 
230
+ # Reset Palette
231
+ self._guiPalette = QApplication.style().standardPalette()
232
+ self._resetGuiColors()
233
+
230
234
  # Main
231
235
  sec = "Main"
232
236
  if parser.has_section(sec):
@@ -256,8 +260,6 @@ class GuiTheme:
256
260
  self._setPalette(parser, sec, "highlightedtext", QPalette.ColorRole.HighlightedText)
257
261
  self._setPalette(parser, sec, "link", QPalette.ColorRole.Link)
258
262
  self._setPalette(parser, sec, "linkvisited", QPalette.ColorRole.LinkVisited)
259
- else:
260
- self._guiPalette = QApplication.style().standardPalette()
261
263
 
262
264
  # GUI
263
265
  sec = "GUI"
@@ -393,6 +395,14 @@ class GuiTheme:
393
395
  # Internal Functions
394
396
  ##
395
397
 
398
+ def _resetGuiColors(self) -> None:
399
+ """Reset GUI colours to default values."""
400
+ self.statNone = QColor(120, 120, 120)
401
+ self.statUnsaved = QColor(200, 15, 39)
402
+ self.statSaved = QColor(2, 133, 37)
403
+ self.helpText = QColor(0, 0, 0)
404
+ return
405
+
396
406
  def _setGuiFont(self) -> None:
397
407
  """Update the GUI's font style from settings."""
398
408
  font = QFont()
novelwriter/guimain.py CHANGED
@@ -944,7 +944,7 @@ class GuiMain(QMainWindow):
944
944
  # Events
945
945
  ##
946
946
 
947
- def closeEvent(self, event: QCloseEvent):
947
+ def closeEvent(self, event: QCloseEvent) -> None:
948
948
  """Capture the closing event of the GUI and call the close
949
949
  function to handle all the close process steps.
950
950
  """
@@ -1201,7 +1201,7 @@ class GuiMain(QMainWindow):
1201
1201
  return
1202
1202
 
1203
1203
  @pyqtSlot()
1204
- def _toggleViewerPanelVisibility(self):
1204
+ def _toggleViewerPanelVisibility(self) -> None:
1205
1205
  """Toggle the visibility of the document viewer panel."""
1206
1206
  CONFIG.showViewerPanel = not CONFIG.showViewerPanel
1207
1207
  self.docViewerPanel.setVisible(CONFIG.showViewerPanel)
novelwriter/shared.py CHANGED
@@ -26,21 +26,21 @@ from __future__ import annotations
26
26
 
27
27
  import logging
28
28
 
29
+ from pathlib import Path
29
30
  from time import time
30
31
  from typing import TYPE_CHECKING, TypeVar
31
- from pathlib import Path
32
32
 
33
33
  from PyQt5.QtCore import QObject, QRunnable, QThreadPool, QTimer, pyqtSignal
34
34
  from PyQt5.QtWidgets import QFileDialog, QMessageBox, QWidget
35
- from novelwriter.common import formatFileFilter
36
35
 
36
+ from novelwriter.common import formatFileFilter
37
37
  from novelwriter.constants import nwFiles
38
38
  from novelwriter.core.spellcheck import NWSpellEnchant
39
39
 
40
40
  if TYPE_CHECKING: # pragma: no cover
41
- from novelwriter.guimain import GuiMain
42
- from novelwriter.gui.theme import GuiTheme
43
41
  from novelwriter.core.project import NWProject
42
+ from novelwriter.gui.theme import GuiTheme
43
+ from novelwriter.guimain import GuiMain
44
44
 
45
45
  logger = logging.getLogger(__name__)
46
46
 
@@ -198,6 +198,7 @@ class SharedData(QObject):
198
198
 
199
199
  def closeProject(self) -> None:
200
200
  """Close the current project."""
201
+ self._closeDialogs()
201
202
  self.project.closeProject(self._idleTime)
202
203
  self._resetProject()
203
204
  self._resetIdleTimer()
@@ -356,6 +357,17 @@ class SharedData(QObject):
356
357
  self._idleTime = 0.0
357
358
  return
358
359
 
360
+ def _closeDialogs(self) -> None:
361
+ """Close non-modal dialogs."""
362
+ from novelwriter.tools.manuscript import GuiManuscript
363
+ from novelwriter.tools.writingstats import GuiWritingStats
364
+
365
+ for widget in self.mainGui.children():
366
+ if isinstance(widget, (GuiManuscript, GuiWritingStats)):
367
+ widget.close()
368
+
369
+ return
370
+
359
371
  # END Class SharedData
360
372
 
361
373
 
@@ -57,6 +57,7 @@ def preProcessText(text: str, keepHeaders: bool = True) -> list[str]:
57
57
  continue
58
58
  if line[0] == ">":
59
59
  line = line.lstrip(">").lstrip(" ")
60
+ if line: # Above block can return empty line (Issue #1816)
60
61
  if line[-1] == "<":
61
62
  line = line.rstrip("<").rstrip(" ")
62
63
  if "[" in line:
@@ -179,7 +179,7 @@ class GuiDictionaries(QDialog):
179
179
  ##
180
180
 
181
181
  @pyqtSlot()
182
- def _doBrowseHunspell(self):
182
+ def _doBrowseHunspell(self) -> None:
183
183
  """Browse for a Free/Libre Office dictionary."""
184
184
  ffilter = formatFileFilter([
185
185
  (self.tr("Free or Libre Office extension"), "*.sox *.oxt"), "*"
@@ -193,7 +193,7 @@ class GuiDictionaries(QDialog):
193
193
  return
194
194
 
195
195
  @pyqtSlot()
196
- def _doImportHunspell(self):
196
+ def _doImportHunspell(self) -> None:
197
197
  """Import a hunspell dictionary from .sox or .oxt file."""
198
198
  procErr = self.tr("Could not process dictionary file")
199
199
  if self._installPath:
@@ -44,9 +44,7 @@ from novelwriter.core.item import NWItem
44
44
  from novelwriter.enum import nwBuildFmt
45
45
  from novelwriter.extensions.modified import NIconToolButton
46
46
  from novelwriter.extensions.simpleprogress import NProgressSimple
47
- from novelwriter.types import (
48
- QtAlignCenter, QtDialogClose, QtRoleAction, QtRoleReject, QtUserRole
49
- )
47
+ from novelwriter.types import QtAlignCenter, QtDialogClose, QtRoleAction, QtRoleReject, QtUserRole
50
48
 
51
49
  logger = logging.getLogger(__name__)
52
50
 
@@ -60,7 +58,7 @@ class GuiManuscriptBuild(QDialog):
60
58
 
61
59
  D_KEY = QtUserRole
62
60
 
63
- def __init__(self, parent: QWidget, build: BuildSettings):
61
+ def __init__(self, parent: QWidget, build: BuildSettings) -> None:
64
62
  super().__init__(parent=parent)
65
63
 
66
64
  logger.debug("Create: GuiManuscriptBuild")
@@ -260,7 +258,7 @@ class GuiManuscriptBuild(QDialog):
260
258
  ##
261
259
 
262
260
  @pyqtSlot("QAbstractButton*")
263
- def _dialogButtonClicked(self, button: QAbstractButton):
261
+ def _dialogButtonClicked(self, button: QAbstractButton) -> None:
264
262
  """Handle button clicks from the dialog button box."""
265
263
  role = self.dlgButtons.buttonRole(button)
266
264
  if role == QtRoleAction:
@@ -273,7 +271,7 @@ class GuiManuscriptBuild(QDialog):
273
271
  return
274
272
 
275
273
  @pyqtSlot()
276
- def _doSelectPath(self):
274
+ def _doSelectPath(self) -> None:
277
275
  """Select a folder for output."""
278
276
  bPath = Path(self.buildPath.text())
279
277
  bPath = bPath if bPath.is_dir() else self._build.lastPath
@@ -285,7 +283,7 @@ class GuiManuscriptBuild(QDialog):
285
283
  return
286
284
 
287
285
  @pyqtSlot()
288
- def _doResetBuildName(self):
286
+ def _doResetBuildName(self) -> None:
289
287
  """Generate a default build name."""
290
288
  bName = f"{SHARED.project.data.name} - {self._build.name}"
291
289
  self.buildName.setText(bName)
@@ -293,7 +291,7 @@ class GuiManuscriptBuild(QDialog):
293
291
  return
294
292
 
295
293
  @pyqtSlot()
296
- def _resetProgress(self):
294
+ def _resetProgress(self) -> None:
297
295
  """Set the progress bar back to 0."""
298
296
  self.buildProgress.setValue(0)
299
297
  return
@@ -328,6 +326,9 @@ class GuiManuscriptBuild(QDialog):
328
326
  ):
329
327
  return False
330
328
 
329
+ # Make sure editor content is saved before we start
330
+ SHARED.mainGui.saveDocument()
331
+
331
332
  docBuild = NWBuildDocument(SHARED.project, self._build)
332
333
  docBuild.queueAll()
333
334
 
@@ -350,7 +351,7 @@ class GuiManuscriptBuild(QDialog):
350
351
  return items[0].data(self.D_KEY)
351
352
  return None
352
353
 
353
- def _saveSettings(self):
354
+ def _saveSettings(self) -> None:
354
355
  """Save the user GUI settings."""
355
356
  winWidth = CONFIG.rpxInt(self.width())
356
357
  winHeight = CONFIG.rpxInt(self.height())
@@ -369,7 +370,7 @@ class GuiManuscriptBuild(QDialog):
369
370
 
370
371
  return
371
372
 
372
- def _populateContentList(self):
373
+ def _populateContentList(self) -> None:
373
374
  """Build the content list."""
374
375
  rootMap = {}
375
376
  filtered = self._build.buildItemFilter(SHARED.project)
@@ -398,7 +399,7 @@ class GuiManuscriptBuild(QDialog):
398
399
 
399
400
  return
400
401
 
401
- def _openOutputFolder(self):
402
+ def _openOutputFolder(self) -> None:
402
403
  """Open the build folder in the system's file explorer."""
403
404
  openExternalPath(Path(self.buildPath.text()))
404
405
  return