novelWriter 2.7b1__py3-none-any.whl → 2.7rc1__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 (56) hide show
  1. novelwriter/__init__.py +17 -4
  2. novelwriter/assets/i18n/project_en_GB.json +1 -0
  3. novelwriter/assets/icons/remix_filled.icons +1 -0
  4. novelwriter/assets/icons/remix_outline.icons +1 -0
  5. novelwriter/assets/images/splash.png +0 -0
  6. novelwriter/assets/manual.pdf +0 -0
  7. novelwriter/assets/manual_fr.pdf +0 -0
  8. novelwriter/assets/sample.zip +0 -0
  9. novelwriter/assets/syntax/snazzy.conf +3 -3
  10. novelwriter/assets/themes/snazzy.conf +48 -0
  11. novelwriter/common.py +10 -1
  12. novelwriter/config.py +96 -25
  13. novelwriter/constants.py +17 -0
  14. novelwriter/core/buildsettings.py +2 -0
  15. novelwriter/core/coretools.py +41 -34
  16. novelwriter/core/docbuild.py +1 -0
  17. novelwriter/core/index.py +25 -1
  18. novelwriter/core/indexdata.py +4 -1
  19. novelwriter/core/item.py +35 -24
  20. novelwriter/core/itemmodel.py +17 -13
  21. novelwriter/core/novelmodel.py +2 -0
  22. novelwriter/core/project.py +14 -14
  23. novelwriter/core/projectdata.py +42 -24
  24. novelwriter/core/projectxml.py +17 -7
  25. novelwriter/core/sessions.py +29 -13
  26. novelwriter/core/tree.py +9 -5
  27. novelwriter/dialogs/docmerge.py +2 -1
  28. novelwriter/dialogs/docsplit.py +6 -3
  29. novelwriter/dialogs/editlabel.py +11 -8
  30. novelwriter/dialogs/preferences.py +37 -26
  31. novelwriter/dialogs/projectsettings.py +3 -0
  32. novelwriter/extensions/configlayout.py +6 -2
  33. novelwriter/extensions/switch.py +16 -15
  34. novelwriter/extensions/switchbox.py +1 -0
  35. novelwriter/formats/tokenizer.py +2 -1
  36. novelwriter/gui/doceditor.py +98 -40
  37. novelwriter/gui/noveltree.py +11 -5
  38. novelwriter/gui/outline.py +9 -1
  39. novelwriter/gui/projtree.py +1 -0
  40. novelwriter/gui/search.py +1 -0
  41. novelwriter/gui/statusbar.py +14 -6
  42. novelwriter/gui/theme.py +25 -10
  43. novelwriter/guimain.py +29 -9
  44. novelwriter/splash.py +74 -0
  45. novelwriter/tools/lipsum.py +2 -1
  46. novelwriter/tools/manuscript.py +1 -1
  47. novelwriter/tools/manussettings.py +38 -13
  48. novelwriter/tools/noveldetails.py +9 -8
  49. novelwriter/tools/welcome.py +1 -0
  50. novelwriter/tools/writingstats.py +68 -45
  51. {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/METADATA +1 -1
  52. {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/RECORD +56 -53
  53. {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/WHEEL +1 -1
  54. {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/entry_points.txt +0 -0
  55. {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/licenses/LICENSE.md +0 -0
  56. {novelwriter-2.7b1.dist-info → novelwriter-2.7rc1.dist-info}/top_level.txt +0 -0
@@ -3,15 +3,16 @@ novelWriter – GUI Document Editor
3
3
  =================================
4
4
 
5
5
  File History:
6
- Created: 2018-09-29 [0.0.1] GuiDocEditor
7
- Created: 2019-04-22 [0.0.1] BackgroundWordCounter
8
- Created: 2019-09-29 [0.2.1] GuiDocEditSearch
9
- Created: 2020-04-25 [0.4.5] GuiDocEditHeader
10
- Rewritten: 2020-06-15 [0.9] GuiDocEditSearch
11
- Created: 2020-06-27 [0.10] GuiDocEditFooter
12
- Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter
13
- Created: 2023-11-06 [2.2b1] MetaCompleter
14
- Created: 2023-11-07 [2.2b1] GuiDocToolBar
6
+ Created: 2018-09-29 [0.0.1] GuiDocEditor
7
+ Created: 2019-04-22 [0.0.1] BackgroundWordCounter
8
+ Created: 2019-09-29 [0.2.1] GuiDocEditSearch
9
+ Created: 2020-04-25 [0.4.5] GuiDocEditHeader
10
+ Rewritten: 2020-06-15 [0.9] GuiDocEditSearch
11
+ Created: 2020-06-27 [0.10] GuiDocEditFooter
12
+ Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter
13
+ Created: 2023-11-06 [2.2b1] MetaCompleter
14
+ Created: 2023-11-07 [2.2b1] GuiDocToolBar
15
+ Extended: 2025-05-18 [2.7rc1] CommandCompleter
15
16
 
16
17
  This file is a part of novelWriter
17
18
  Copyright (C) 2018 Veronica Berglyd Olsen and novelWriter contributors
@@ -55,7 +56,9 @@ from novelwriter import CONFIG, SHARED
55
56
  from novelwriter.common import (
56
57
  decodeMimeHandles, fontMatcher, minmax, qtAddAction, qtLambda, transferCase
57
58
  )
58
- from novelwriter.constants import nwConst, nwKeyWords, nwShortcode, nwUnicode
59
+ from novelwriter.constants import (
60
+ nwConst, nwKeyWords, nwLabels, nwShortcode, nwStats, nwUnicode, trStats
61
+ )
59
62
  from novelwriter.core.document import NWDocument
60
63
  from novelwriter.enum import (
61
64
  nwChange, nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass,
@@ -147,7 +150,7 @@ class GuiDocEditor(QPlainTextEdit):
147
150
  self._autoReplace = TextAutoReplace()
148
151
 
149
152
  # Completer
150
- self._completer = MetaCompleter(self)
153
+ self._completer = CommandCompleter(self)
151
154
  self._completer.complete.connect(self._insertCompletion)
152
155
 
153
156
  # Create Custom Document
@@ -321,6 +324,7 @@ class GuiDocEditor(QPlainTextEdit):
321
324
  """
322
325
  # Auto-Replace
323
326
  self._autoReplace.initSettings()
327
+ self.docFooter.initSettings()
324
328
 
325
329
  # Reload spell check and dictionaries
326
330
  SHARED.updateSpellCheckLanguage()
@@ -1076,13 +1080,16 @@ class GuiDocEditor(QPlainTextEdit):
1076
1080
 
1077
1081
  if (block := self._qDocument.findBlock(pos)).isValid():
1078
1082
  text = block.text()
1079
- if text.startswith("@") and added + removed == 1:
1083
+ if text and text[0] in "@%" and added + removed == 1:
1080
1084
  # Only run on single character changes, or it will trigger
1081
1085
  # at unwanted times when other changes are made to the document
1082
1086
  cursor = self.textCursor()
1083
1087
  bPos = cursor.positionInBlock()
1084
1088
  if bPos > 0 and (viewport := self.viewport()):
1085
- show = self._completer.updateText(text, bPos)
1089
+ if text[0] == "@":
1090
+ show = self._completer.updateMetaText(text, bPos)
1091
+ else:
1092
+ show = self._completer.updateCommentText(text, bPos)
1086
1093
  point = self.cursorRect().bottomRight()
1087
1094
  self._completer.move(viewport.mapToGlobal(point))
1088
1095
  self._completer.setVisible(show)
@@ -1233,7 +1240,8 @@ class GuiDocEditor(QPlainTextEdit):
1233
1240
  """Process the word counter's finished signal."""
1234
1241
  if self._docHandle and self._nwItem:
1235
1242
  logger.debug("Updating word count")
1236
- needsRefresh = wCount != self._nwItem.wordCount
1243
+ mCount = cCount if CONFIG.useCharCount else wCount
1244
+ needsRefresh = mCount != self._nwItem.mainCount
1237
1245
  self._nwItem.setCharCount(cCount)
1238
1246
  self._nwItem.setWordCount(wCount)
1239
1247
  self._nwItem.setParaCount(pCount)
@@ -1241,7 +1249,7 @@ class GuiDocEditor(QPlainTextEdit):
1241
1249
  self._nwItem.notifyToRefresh()
1242
1250
  if not self.textCursor().hasSelection():
1243
1251
  # Selection counter should take precedence (#2155)
1244
- self.docFooter.updateWordCount(wCount, False)
1252
+ self.docFooter.updateMainCount(mCount, False)
1245
1253
  return
1246
1254
 
1247
1255
  @pyqtSlot()
@@ -1254,7 +1262,7 @@ class GuiDocEditor(QPlainTextEdit):
1254
1262
  self._timerSel.start()
1255
1263
  else:
1256
1264
  self._timerSel.stop()
1257
- self.docFooter.updateWordCount(0, False)
1265
+ self.docFooter.updateMainCount(0, False)
1258
1266
  return
1259
1267
 
1260
1268
  @pyqtSlot()
@@ -1271,8 +1279,7 @@ class GuiDocEditor(QPlainTextEdit):
1271
1279
  def _updateSelCounts(self, cCount: int, wCount: int, pCount: int) -> None:
1272
1280
  """Update the counts on the counter's finished signal."""
1273
1281
  if self._docHandle and self._nwItem:
1274
- logger.debug("User selected %d words", wCount)
1275
- self.docFooter.updateWordCount(wCount, True)
1282
+ self.docFooter.updateMainCount(cCount if CONFIG.useCharCount else wCount, True)
1276
1283
  self._timerSel.stop()
1277
1284
  return
1278
1285
 
@@ -2070,13 +2077,13 @@ class GuiDocEditor(QPlainTextEdit):
2070
2077
  return
2071
2078
 
2072
2079
 
2073
- class MetaCompleter(QMenu):
2074
- """GuiWidget: Meta Completer Menu
2080
+ class CommandCompleter(QMenu):
2081
+ """GuiWidget: Command Completer Menu
2075
2082
 
2076
2083
  This is a context menu with options populated from the user's
2077
- defined tags. It also helps to type the meta data keyword on a new
2078
- line starting with an @. The updateText function should be called on
2079
- every keystroke on a line starting with @.
2084
+ defined tags and keys. It also helps to type the meta data keyword
2085
+ on a new line starting with @ or %. The update functions should be
2086
+ called on every keystroke on a line starting with @ or %.
2080
2087
  """
2081
2088
 
2082
2089
  complete = pyqtSignal(int, int, str)
@@ -2085,7 +2092,7 @@ class MetaCompleter(QMenu):
2085
2092
  super().__init__(parent=parent)
2086
2093
  return
2087
2094
 
2088
- def updateText(self, text: str, pos: int) -> bool:
2095
+ def updateMetaText(self, text: str, pos: int) -> bool:
2089
2096
  """Update the menu options based on the line of text."""
2090
2097
  self.clear()
2091
2098
  kw, sep, _ = text.partition(":")
@@ -2093,7 +2100,7 @@ class MetaCompleter(QMenu):
2093
2100
  offset = 0
2094
2101
  length = len(kw.rstrip())
2095
2102
  suffix = "" if sep else ":"
2096
- options = list(filter(
2103
+ options = sorted(filter(
2097
2104
  lambda x: x.startswith(kw.rstrip()), nwKeyWords.VALID_KEYS
2098
2105
  ))
2099
2106
  else:
@@ -2105,7 +2112,7 @@ class MetaCompleter(QMenu):
2105
2112
  offset = tPos[index] if lookup else pos
2106
2113
  length = len(lookup)
2107
2114
  suffix = ""
2108
- options = list(filter(
2115
+ options = sorted(filter(
2109
2116
  lambda x: lookup in x.lower(), SHARED.project.index.getClassTags(
2110
2117
  nwKeyWords.KEY_CLASS.get(kw.strip())
2111
2118
  )
@@ -2114,13 +2121,57 @@ class MetaCompleter(QMenu):
2114
2121
  if not options:
2115
2122
  return False
2116
2123
 
2117
- for value in sorted(options):
2124
+ for value in options:
2118
2125
  rep = value + suffix
2119
2126
  action = qtAddAction(self, value)
2120
2127
  action.triggered.connect(qtLambda(self._emitComplete, offset, length, rep))
2121
2128
 
2122
2129
  return True
2123
2130
 
2131
+ def updateCommentText(self, text: str, pos: int) -> bool:
2132
+ """Update the menu options based on the line of text."""
2133
+ self.clear()
2134
+ cmd, sep, _ = text.partition(":")
2135
+ if pos <= len(cmd):
2136
+ clean = text[1:].lstrip()[:6].lower()
2137
+ if clean[:6] == "story.":
2138
+ pre, _, key = cmd.partition(".")
2139
+ offset = len(pre) + 1
2140
+ length = len(key)
2141
+ suffix = "" if sep else ": "
2142
+ options = sorted(filter(
2143
+ lambda x: x.startswith(key.rstrip()),
2144
+ SHARED.project.index.getStoryKeys(),
2145
+ ))
2146
+ elif clean[:5] == "note.":
2147
+ pre, _, key = cmd.partition(".")
2148
+ offset = len(pre) + 1
2149
+ length = len(key)
2150
+ suffix = "" if sep else ": "
2151
+ options = sorted(filter(
2152
+ lambda x: x.startswith(key.rstrip()),
2153
+ SHARED.project.index.getNoteKeys(),
2154
+ ))
2155
+ elif pos < 12:
2156
+ offset = 0
2157
+ length = len(cmd.rstrip())
2158
+ suffix = ""
2159
+ options = list(filter(
2160
+ lambda x: x.startswith(cmd.rstrip()),
2161
+ ["%Synopsis: ", "%Short: ", "%Story", "%Note"],
2162
+ ))
2163
+ else:
2164
+ return False
2165
+
2166
+ if options:
2167
+ for value in options:
2168
+ rep = value + suffix
2169
+ action = qtAddAction(self, rep.rstrip(":. "))
2170
+ action.triggered.connect(qtLambda(self._emitComplete, offset, length, rep))
2171
+ return True
2172
+
2173
+ return False
2174
+
2124
2175
  ##
2125
2176
  # Events
2126
2177
  ##
@@ -3045,9 +3096,9 @@ class GuiDocEditFooter(QWidget):
3045
3096
  fPx = int(0.9*SHARED.theme.fontPixelSize)
3046
3097
 
3047
3098
  # Cached Translations
3099
+ self.initSettings()
3048
3100
  self._trLineCount = self.tr("Line: {0} ({1})")
3049
- self._trWordCount = self.tr("Words: {0} ({1})")
3050
- self._trSelectCount = self.tr("Words: {0} selected")
3101
+ self._trSelectCount = self.tr("Selected: {0}")
3051
3102
 
3052
3103
  # Main Widget Settings
3053
3104
  self.setContentsMargins(0, 0, 0, 0)
@@ -3108,7 +3159,7 @@ class GuiDocEditFooter(QWidget):
3108
3159
  self.updateTheme()
3109
3160
 
3110
3161
  # Initialise Info
3111
- self.updateWordCount(0, False)
3162
+ self.updateMainCount(0, False)
3112
3163
 
3113
3164
  logger.debug("Ready: GuiDocEditFooter")
3114
3165
 
@@ -3118,6 +3169,13 @@ class GuiDocEditFooter(QWidget):
3118
3169
  # Methods
3119
3170
  ##
3120
3171
 
3172
+ def initSettings(self) -> None:
3173
+ """Apply user settings."""
3174
+ self._trMainCount = trStats(nwLabels.STATS_DISPLAY[
3175
+ nwStats.CHARS if CONFIG.useCharCount else nwStats.WORDS
3176
+ ])
3177
+ return
3178
+
3121
3179
  def updateFont(self) -> None:
3122
3180
  """Update the font settings."""
3123
3181
  self.setFont(SHARED.theme.guiFont)
@@ -3162,7 +3220,7 @@ class GuiDocEditFooter(QWidget):
3162
3220
  self._tItem = SHARED.project.tree[self._docHandle]
3163
3221
 
3164
3222
  self.updateInfo()
3165
- self.updateWordCount(0, False)
3223
+ self.updateMainCount(0, False)
3166
3224
 
3167
3225
  return
3168
3226
 
@@ -3193,15 +3251,15 @@ class GuiDocEditFooter(QWidget):
3193
3251
  )
3194
3252
  return
3195
3253
 
3196
- def updateWordCount(self, wCount: int, selection: bool) -> None:
3197
- """Update word counter information."""
3198
- if selection and wCount:
3199
- wText = self._trSelectCount.format(f"{wCount:n}")
3254
+ def updateMainCount(self, count: int, selection: bool) -> None:
3255
+ """Update main counter information."""
3256
+ if selection and count:
3257
+ text = self._trSelectCount.format(f"{count:n}")
3200
3258
  elif self._tItem:
3201
- wCount = self._tItem.wordCount
3202
- wDiff = wCount - self._tItem.initCount
3203
- wText = self._trWordCount.format(f"{wCount:n}", f"{wDiff:+n}")
3259
+ count = self._tItem.mainCount
3260
+ diff = count - self._tItem.initCount
3261
+ text = self._trMainCount.format(f"{count:n}", f"{diff:+n}")
3204
3262
  else:
3205
- wText = self._trWordCount.format("0", "+0")
3206
- self.wordsText.setText(wText)
3263
+ text = self._trMainCount.format("0", "+0")
3264
+ self.wordsText.setText(text)
3207
3265
  return
@@ -80,6 +80,7 @@ class GuiNovelView(QWidget):
80
80
  # Function Mappings
81
81
  self.setActive = self.novelBar.setActive
82
82
  self.getSelectedHandle = self.novelTree.getSelectedHandle
83
+ self.refreshCurrentTree = self.novelBar.forceRefreshNovelTree
83
84
 
84
85
  return
85
86
 
@@ -209,7 +210,7 @@ class GuiNovelToolBar(QWidget):
209
210
  # Refresh Button
210
211
  self.tbRefresh = NIconToolButton(self, iSz)
211
212
  self.tbRefresh.setToolTip(self.tr("Refresh"))
212
- self.tbRefresh.clicked.connect(self._forceRefreshNovelTree)
213
+ self.tbRefresh.clicked.connect(self.forceRefreshNovelTree)
213
214
 
214
215
  # More Options Menu
215
216
  self.mMore = QMenu(self)
@@ -274,7 +275,7 @@ class GuiNovelToolBar(QWidget):
274
275
  self.novelValue.updateTheme()
275
276
  self.tbNovel.setVisible(self.novelValue.count() > 1)
276
277
 
277
- self._forceRefreshNovelTree()
278
+ self.forceRefreshNovelTree()
278
279
 
279
280
  return
280
281
 
@@ -298,6 +299,7 @@ class GuiNovelToolBar(QWidget):
298
299
  self.novelValue.setHandle(rootHandle)
299
300
  SHARED.project.data.setLastHandle(rootHandle, "novel")
300
301
  self.novelView.setCurrentNovel(rootHandle)
302
+ self.novelView.novelTree.setAccessibleName(self.novelValue.currentText())
301
303
  return
302
304
 
303
305
  def setLastColType(self, colType: nwNovelExtra, doRefresh: bool = True) -> None:
@@ -305,7 +307,7 @@ class GuiNovelToolBar(QWidget):
305
307
  self.aLastCol[colType].setChecked(True)
306
308
  self.novelView.novelTree.setLastColType(colType)
307
309
  if doRefresh:
308
- self._forceRefreshNovelTree()
310
+ self.forceRefreshNovelTree()
309
311
  self.novelView.novelTree.resizeColumns()
310
312
  return
311
313
 
@@ -323,11 +325,11 @@ class GuiNovelToolBar(QWidget):
323
325
  return
324
326
 
325
327
  ##
326
- # Private Slots
328
+ # Public Slots
327
329
  ##
328
330
 
329
331
  @pyqtSlot()
330
- def _forceRefreshNovelTree(self) -> None:
332
+ def forceRefreshNovelTree(self) -> None:
331
333
  """Rebuild the current tree."""
332
334
  if tHandle := self.novelValue.handle:
333
335
  self.novelView.setCurrentNovel(tHandle)
@@ -335,6 +337,10 @@ class GuiNovelToolBar(QWidget):
335
337
  self._refresh[tHandle] = False
336
338
  return
337
339
 
340
+ ##
341
+ # Private Slots
342
+ ##
343
+
338
344
  @pyqtSlot(str)
339
345
  def _refreshNovelTree(self, tHandle: str) -> None:
340
346
  """Refresh or schedule refresh of a novel tree."""
@@ -751,9 +751,13 @@ class GuiOutlineTree(QTreeWidget):
751
751
  def _dumpNovelData(self, rootHandle: str | None) -> list[list[str | int]]:
752
752
  """Dump all novel data into a table."""
753
753
  sLabel = SHARED.project.localLookup("Story Structure")
754
+ nLabel = SHARED.project.localLookup("Note")
754
755
  sKeys = sorted(SHARED.project.index.getStoryKeys())
756
+ nKeys = sorted(SHARED.project.index.getNoteKeys())
755
757
  sMatch = [f"story.{k}" for k in sKeys]
758
+ nMatch = [f"note.{k}" for k in nKeys]
756
759
  sHeaders = [f"{sLabel} ({k})" for k in sKeys]
760
+ nHeaders = [f"{nLabel} ({k})" for k in nKeys]
757
761
 
758
762
  data: list[list[str | int]] = [[
759
763
  "H",
@@ -777,6 +781,7 @@ class GuiOutlineTree(QTreeWidget):
777
781
  trConst(nwLabels.OUTLINE_COLS[nwOutline.MENTION]),
778
782
  trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP]),
779
783
  *sHeaders,
784
+ *nHeaders,
780
785
  ]]
781
786
 
782
787
  for _, tHandle, sTitle, novIdx in SHARED.project.index.novelStructure(
@@ -784,7 +789,9 @@ class GuiOutlineTree(QTreeWidget):
784
789
  ):
785
790
  if novIdx.level != "H0" and (nwItem := SHARED.project.tree[tHandle]):
786
791
  refs = SHARED.project.index.getReferences(tHandle, sTitle)
787
- story = {k: v for k, v in novIdx.comments.items() if k in sMatch}.values()
792
+ comments = dict(novIdx.comments.items())
793
+ story = [comments.get(k, "") for k in sMatch]
794
+ notes = [comments.get(k, "") for k in nMatch]
788
795
  data.append([
789
796
  novIdx.level,
790
797
  novIdx.title,
@@ -807,6 +814,7 @@ class GuiOutlineTree(QTreeWidget):
807
814
  ", ".join(refs[nwKeyWords.MENTION_KEY]),
808
815
  novIdx.synopsis,
809
816
  *story,
817
+ *notes,
810
818
  ])
811
819
 
812
820
  return data
@@ -254,6 +254,7 @@ class GuiProjectToolBar(QWidget):
254
254
  self.viewLabel.setFont(SHARED.theme.guiFontB)
255
255
  self.viewLabel.setContentsMargins(0, 0, 0, 0)
256
256
  self.viewLabel.setSizePolicy(QtSizeExpanding, QtSizeExpanding)
257
+ self.projTree.setAccessibleName(self.viewLabel.text())
257
258
 
258
259
  # Quick Links
259
260
  self.mQuick = QMenu(self)
novelwriter/gui/search.py CHANGED
@@ -125,6 +125,7 @@ class GuiProjectSearch(QWidget):
125
125
  self.searchResult.setAllColumnsShowFocus(True)
126
126
  self.searchResult.itemDoubleClicked.connect(self._searchResultDoubleClicked)
127
127
  self.searchResult.itemSelectionChanged.connect(self._searchResultSelected)
128
+ self.searchResult.setAccessibleName(self.viewLabel.text())
128
129
 
129
130
  if header := self.searchResult.header():
130
131
  header.setStretchLastSection(False)
@@ -33,7 +33,7 @@ from PyQt6.QtWidgets import QApplication, QLabel, QStatusBar, QWidget
33
33
 
34
34
  from novelwriter import CONFIG, SHARED
35
35
  from novelwriter.common import formatTime
36
- from novelwriter.constants import nwConst
36
+ from novelwriter.constants import nwConst, nwLabels, nwStats, trStats
37
37
  from novelwriter.extensions.modified import NClickableLabel
38
38
  from novelwriter.extensions.statusled import StatusLED
39
39
 
@@ -108,11 +108,22 @@ class GuiMainStatus(QStatusBar):
108
108
 
109
109
  logger.debug("Ready: GuiMainStatus")
110
110
 
111
+ self.initSettings()
111
112
  self.updateTheme()
112
113
  self.clearStatus()
113
114
 
114
115
  return
115
116
 
117
+ def initSettings(self) -> None:
118
+ """Apply user settings."""
119
+ if CONFIG.useCharCount:
120
+ self._trStatsCount = trStats(nwLabels.STATS_DISPLAY[nwStats.CHARS])
121
+ self._trStatsTip = self.tr("Total character count (session change)")
122
+ else:
123
+ self._trStatsCount = trStats(nwLabels.STATS_DISPLAY[nwStats.WORDS])
124
+ self._trStatsTip = self.tr("Total word count (session change)")
125
+ return
126
+
116
127
  def clearStatus(self) -> None:
117
128
  """Reset all widgets on the status bar to default values."""
118
129
  self.setRefTime(-1.0)
@@ -173,11 +184,8 @@ class GuiMainStatus(QStatusBar):
173
184
 
174
185
  def setProjectStats(self, pWC: int, sWC: int) -> None:
175
186
  """Update the current project statistics."""
176
- self.statsText.setText(self.tr("Words: {0} ({1})").format(f"{pWC:n}", f"{sWC:+n}"))
177
- if CONFIG.incNotesWCount:
178
- self.statsText.setToolTip(self.tr("Project word count (session change)"))
179
- else:
180
- self.statsText.setToolTip(self.tr("Novel word count (session change)"))
187
+ self.statsText.setText(self._trStatsCount.format(f"{pWC:n}", f"{sWC:+n}"))
188
+ self.statsText.setToolTip(self._trStatsTip)
181
189
  return
182
190
 
183
191
  def updateTime(self, idleTime: float = 0.0) -> None:
novelwriter/gui/theme.py CHANGED
@@ -130,14 +130,6 @@ class GuiTheme:
130
130
  self._availSyntax: dict[str, Path] = {}
131
131
  self._styleSheets: dict[str, str] = {}
132
132
 
133
- _listConf(self._availSyntax, CONFIG.assetPath("syntax"), ".conf")
134
- _listConf(self._availThemes, CONFIG.assetPath("themes"), ".conf")
135
- _listConf(self._availSyntax, CONFIG.dataPath("syntax"), ".conf")
136
- _listConf(self._availThemes, CONFIG.dataPath("themes"), ".conf")
137
-
138
- self.loadTheme()
139
- self.loadSyntax()
140
-
141
133
  # Icon Functions
142
134
  self.getIcon = self.iconCache.getIcon
143
135
  self.getPixmap = self.iconCache.getPixmap
@@ -186,6 +178,15 @@ class GuiTheme:
186
178
  logger.debug("Text 'N' Height: %d", self.textNHeight)
187
179
  logger.debug("Text 'N' Width: %d", self.textNWidth)
188
180
 
181
+ # Process Themes
182
+ _listConf(self._availSyntax, CONFIG.assetPath("syntax"), ".conf")
183
+ _listConf(self._availThemes, CONFIG.assetPath("themes"), ".conf")
184
+ _listConf(self._availSyntax, CONFIG.dataPath("syntax"), ".conf")
185
+ _listConf(self._availThemes, CONFIG.dataPath("themes"), ".conf")
186
+
187
+ self.loadTheme()
188
+ self.loadSyntax()
189
+
189
190
  return
190
191
 
191
192
  ##
@@ -218,6 +219,7 @@ class GuiTheme:
218
219
  logger.error("Could not load GUI theme")
219
220
  return False
220
221
 
222
+ CONFIG.splashMessage("Loading GUI theme ...")
221
223
  logger.info("Loading GUI theme '%s'", theme)
222
224
  parser = NWConfigParser()
223
225
  try:
@@ -357,6 +359,8 @@ class GuiTheme:
357
359
  QApplication.setPalette(self._guiPalette)
358
360
  self._buildStyleSheets(self._guiPalette)
359
361
 
362
+ CONFIG.splashMessage(f"Loaded GUI theme: {meta.name}")
363
+
360
364
  return True
361
365
 
362
366
  def loadSyntax(self) -> bool:
@@ -371,6 +375,7 @@ class GuiTheme:
371
375
  logger.error("Could not load syntax theme")
372
376
  return False
373
377
 
378
+ CONFIG.splashMessage("Loading syntax theme ...")
374
379
  logger.info("Loading syntax theme '%s'", theme)
375
380
  parser = NWConfigParser()
376
381
  try:
@@ -418,6 +423,8 @@ class GuiTheme:
418
423
  syntax.mod = self._parseColor(parser, sec, "modifier")
419
424
  syntax.mark = self._parseColor(parser, sec, "texthighlight")
420
425
 
426
+ CONFIG.splashMessage(f"Loaded syntax theme: {meta.name}")
427
+
421
428
  self.syntaxMeta = meta
422
429
  self.syntaxTheme = syntax
423
430
 
@@ -633,6 +640,7 @@ class GuiIcons:
633
640
  logger.error("Could not load icon theme")
634
641
  return False
635
642
 
643
+ CONFIG.splashMessage("Loading icon theme ...")
636
644
  logger.info("Loading icon theme '%s'", theme)
637
645
  try:
638
646
  meta = ThemeMeta()
@@ -656,6 +664,9 @@ class GuiIcons:
656
664
  logException()
657
665
  return False
658
666
 
667
+ CONFIG.splashMessage(f"Loaded icon theme: {meta.name}")
668
+ CONFIG.splashMessage("Generating additional icons ...")
669
+
659
670
  # Set colour overrides for project item icons
660
671
  if (override := CONFIG.iconColTree) != "theme":
661
672
  color = self._svgColors.get(override, b"#000000")
@@ -668,6 +679,10 @@ class GuiIcons:
668
679
  self._svgColors["scene"] = color
669
680
  self._svgColors["note"] = color
670
681
 
682
+ # Populate generated icons cache
683
+ self.getHeaderDecoration(0)
684
+ self.getHeaderDecorationNarrow(0)
685
+
671
686
  return True
672
687
 
673
688
  def setIconColor(self, key: str, color: QColor) -> None:
@@ -822,8 +837,8 @@ class GuiIcons:
822
837
  ##
823
838
 
824
839
  def _loadIcon(self, name: str, color: str | None = None, w: int = 24, h: int = 24) -> QIcon:
825
- """Load an icon from the assets themes folder. Is guaranteed to
826
- return a QIcon.
840
+ """Load an icon from the assets themes folder. This function is
841
+ guaranteed to return a QIcon.
827
842
  """
828
843
  # If we just want the app icons, return right away
829
844
  if name == "novelwriter":
novelwriter/guimain.py CHANGED
@@ -310,11 +310,11 @@ class GuiMain(QMainWindow):
310
310
  self.asProjTimer.start()
311
311
  self.asDocTimer.start()
312
312
  self.mainStatus.clearStatus()
313
- self.showNormal()
314
313
 
315
314
  logger.debug("Ready: GUI")
316
315
  logger.info("novelWriter is ready ...")
317
316
  self.mainStatus.setStatusMessage(self.tr("novelWriter is ready ..."))
317
+ CONFIG.splashMessage("novelWriter is ready ...")
318
318
 
319
319
  return
320
320
 
@@ -340,6 +340,15 @@ class GuiMain(QMainWindow):
340
340
  logger.info("Command line path: %s", cmdOpen)
341
341
  self.openProject(cmdOpen)
342
342
 
343
+ # Add a small delay for the window coordinates to be ready
344
+ # before showing any dialogs
345
+ QTimer.singleShot(50, self.showPostLaunchDialogs)
346
+
347
+ return
348
+
349
+ @pyqtSlot()
350
+ def showPostLaunchDialogs(self) -> None:
351
+ """Show post launch dialogs."""
343
352
  if not SHARED.hasProject:
344
353
  self.showWelcomeDialog()
345
354
 
@@ -1039,8 +1048,10 @@ class GuiMain(QMainWindow):
1039
1048
  self.initMain()
1040
1049
  self.saveDocument()
1041
1050
 
1042
- if tree:
1051
+ if tree and not theme:
1052
+ # These are also updated by a theme refresh
1043
1053
  SHARED.project.tree.refreshAllItems()
1054
+ self.novelView.refreshCurrentTree()
1044
1055
 
1045
1056
  if theme:
1046
1057
  SHARED.theme.loadTheme()
@@ -1066,6 +1077,7 @@ class GuiMain(QMainWindow):
1066
1077
  self.projView.initSettings()
1067
1078
  self.novelView.initSettings()
1068
1079
  self.outlineView.initSettings()
1080
+ self.mainStatus.initSettings()
1069
1081
 
1070
1082
  # Force update of word count
1071
1083
  self._lastTotalCount = 0
@@ -1252,15 +1264,23 @@ class GuiMain(QMainWindow):
1252
1264
  if self._lastTotalCount != currentTotalCount:
1253
1265
  self._lastTotalCount = currentTotalCount
1254
1266
 
1255
- SHARED.project.updateWordCounts()
1267
+ SHARED.project.updateCounts()
1256
1268
  if CONFIG.incNotesWCount:
1257
- iTotal = sum(SHARED.project.data.initCounts)
1258
- cTotal = sum(SHARED.project.data.currCounts)
1259
- self.mainStatus.setProjectStats(cTotal, cTotal - iTotal)
1269
+ if CONFIG.useCharCount:
1270
+ iTotal = sum(SHARED.project.data.initCounts[2:])
1271
+ cTotal = sum(SHARED.project.data.currCounts[2:])
1272
+ else:
1273
+ iTotal = sum(SHARED.project.data.initCounts[:2])
1274
+ cTotal = sum(SHARED.project.data.currCounts[:2])
1260
1275
  else:
1261
- iNovel, _ = SHARED.project.data.initCounts
1262
- cNovel, _ = SHARED.project.data.currCounts
1263
- self.mainStatus.setProjectStats(cNovel, cNovel - iNovel)
1276
+ if CONFIG.useCharCount:
1277
+ iTotal = SHARED.project.data.initCounts[2]
1278
+ cTotal = SHARED.project.data.currCounts[2]
1279
+ else:
1280
+ iTotal = SHARED.project.data.initCounts[0]
1281
+ cTotal = SHARED.project.data.currCounts[0]
1282
+
1283
+ self.mainStatus.setProjectStats(cTotal, cTotal - iTotal)
1264
1284
 
1265
1285
  return
1266
1286