novelWriter 2.7b1__py3-none-any.whl → 2.7.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 (81) hide show
  1. novelwriter/__init__.py +17 -4
  2. novelwriter/assets/i18n/nw_cs_CZ.qm +0 -0
  3. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  4. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  5. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  6. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  7. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  8. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  9. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  10. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  11. novelwriter/assets/i18n/nw_pl_PL.qm +0 -0
  12. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  13. novelwriter/assets/i18n/nw_ru_RU.qm +0 -0
  14. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  15. novelwriter/assets/i18n/project_cs_CZ.json +2 -0
  16. novelwriter/assets/i18n/project_de_DE.json +3 -1
  17. novelwriter/assets/i18n/project_en_GB.json +1 -0
  18. novelwriter/assets/i18n/project_en_US.json +2 -0
  19. novelwriter/assets/i18n/project_it_IT.json +2 -0
  20. novelwriter/assets/i18n/project_ja_JP.json +2 -0
  21. novelwriter/assets/i18n/project_nb_NO.json +2 -0
  22. novelwriter/assets/i18n/project_nn_NO.json +5 -0
  23. novelwriter/assets/i18n/project_pl_PL.json +2 -0
  24. novelwriter/assets/i18n/project_pt_BR.json +2 -0
  25. novelwriter/assets/i18n/project_ru_RU.json +2 -0
  26. novelwriter/assets/i18n/project_zh_CN.json +2 -0
  27. novelwriter/assets/icons/remix_filled.icons +1 -0
  28. novelwriter/assets/icons/remix_outline.icons +1 -0
  29. novelwriter/assets/images/splash.png +0 -0
  30. novelwriter/assets/manual.pdf +0 -0
  31. novelwriter/assets/manual_fr.pdf +0 -0
  32. novelwriter/assets/sample.zip +0 -0
  33. novelwriter/assets/syntax/snazzy.conf +3 -3
  34. novelwriter/assets/text/credits_en.htm +6 -0
  35. novelwriter/assets/themes/snazzy.conf +48 -0
  36. novelwriter/common.py +10 -1
  37. novelwriter/config.py +96 -25
  38. novelwriter/constants.py +21 -0
  39. novelwriter/core/buildsettings.py +3 -1
  40. novelwriter/core/coretools.py +41 -34
  41. novelwriter/core/docbuild.py +1 -0
  42. novelwriter/core/index.py +35 -5
  43. novelwriter/core/indexdata.py +4 -1
  44. novelwriter/core/item.py +35 -24
  45. novelwriter/core/itemmodel.py +17 -13
  46. novelwriter/core/novelmodel.py +2 -0
  47. novelwriter/core/project.py +14 -14
  48. novelwriter/core/projectdata.py +42 -24
  49. novelwriter/core/projectxml.py +17 -7
  50. novelwriter/core/sessions.py +29 -13
  51. novelwriter/core/tree.py +9 -5
  52. novelwriter/dialogs/docmerge.py +2 -1
  53. novelwriter/dialogs/docsplit.py +6 -3
  54. novelwriter/dialogs/editlabel.py +11 -8
  55. novelwriter/dialogs/preferences.py +37 -26
  56. novelwriter/dialogs/projectsettings.py +3 -0
  57. novelwriter/extensions/configlayout.py +6 -2
  58. novelwriter/extensions/switch.py +16 -15
  59. novelwriter/extensions/switchbox.py +1 -0
  60. novelwriter/formats/tokenizer.py +2 -1
  61. novelwriter/gui/doceditor.py +106 -47
  62. novelwriter/gui/noveltree.py +11 -5
  63. novelwriter/gui/outline.py +9 -1
  64. novelwriter/gui/projtree.py +1 -0
  65. novelwriter/gui/search.py +1 -0
  66. novelwriter/gui/statusbar.py +14 -6
  67. novelwriter/gui/theme.py +25 -10
  68. novelwriter/guimain.py +29 -9
  69. novelwriter/splash.py +74 -0
  70. novelwriter/tools/lipsum.py +2 -1
  71. novelwriter/tools/manuscript.py +1 -1
  72. novelwriter/tools/manussettings.py +52 -20
  73. novelwriter/tools/noveldetails.py +9 -8
  74. novelwriter/tools/welcome.py +1 -0
  75. novelwriter/tools/writingstats.py +68 -45
  76. {novelwriter-2.7b1.dist-info → novelwriter-2.7.1.dist-info}/METADATA +2 -2
  77. {novelwriter-2.7b1.dist-info → novelwriter-2.7.1.dist-info}/RECORD +81 -78
  78. {novelwriter-2.7b1.dist-info → novelwriter-2.7.1.dist-info}/WHEEL +1 -1
  79. {novelwriter-2.7b1.dist-info → novelwriter-2.7.1.dist-info}/entry_points.txt +0 -0
  80. {novelwriter-2.7b1.dist-info → novelwriter-2.7.1.dist-info}/licenses/LICENSE.md +0 -0
  81. {novelwriter-2.7b1.dist-info → novelwriter-2.7.1.dist-info}/top_level.txt +0 -0
@@ -66,8 +66,8 @@ class NWProjectData:
66
66
  self._spellLang = None
67
67
 
68
68
  # Project Dictionaries
69
- self._initCounts = [0, 0]
70
- self._currCounts = [0, 0]
69
+ self._initCounts = [0, 0, 0, 0]
70
+ self._currCounts = [0, 0, 0, 0]
71
71
  self._lastHandle: dict[str, str | None] = {
72
72
  "editor": None,
73
73
  "viewer": None,
@@ -148,18 +148,18 @@ class NWProjectData:
148
148
  return self._spellLang
149
149
 
150
150
  @property
151
- def initCounts(self) -> tuple[int, int]:
152
- """Return the initial count of words for novel and note
153
- documents.
151
+ def initCounts(self) -> tuple[int, int, int, int]:
152
+ """Return the initial count of words and characters for novel
153
+ and note documents.
154
154
  """
155
- return self._initCounts[0], self._initCounts[1]
155
+ return self._initCounts[0], self._initCounts[1], self._initCounts[2], self._initCounts[3]
156
156
 
157
157
  @property
158
- def currCounts(self) -> tuple[int, int]:
159
- """Return the current count of words for novel and note
160
- documents.
158
+ def currCounts(self) -> tuple[int, int, int, int]:
159
+ """Return the current count of words and characters for novel
160
+ and note documents.
161
161
  """
162
- return self._currCounts[0], self._currCounts[1]
162
+ return self._currCounts[0], self._currCounts[1], self._currCounts[2], self._currCounts[3]
163
163
 
164
164
  @property
165
165
  def lastHandle(self) -> dict[str, str | None]:
@@ -301,22 +301,40 @@ class NWProjectData:
301
301
  self._project.setProjectChanged(True)
302
302
  return
303
303
 
304
- def setInitCounts(self, novel: Any = None, notes: Any = None) -> None:
305
- """Set the word count totals for novel and note files."""
306
- if novel is not None:
307
- self._initCounts[0] = checkInt(novel, 0)
308
- self._currCounts[0] = checkInt(novel, 0)
309
- if notes is not None:
310
- self._initCounts[1] = checkInt(notes, 0)
311
- self._currCounts[1] = checkInt(notes, 0)
304
+ def setInitCounts(
305
+ self, wNovel: Any = None, wNotes: Any = None, cNovel: Any = None, cNotes: Any = None
306
+ ) -> None:
307
+ """Set the count totals for novel and note files."""
308
+ if wNovel is not None:
309
+ count = checkInt(wNovel, 0)
310
+ self._initCounts[0] = count
311
+ self._currCounts[0] = count
312
+ if wNotes is not None:
313
+ count = checkInt(wNotes, 0)
314
+ self._initCounts[1] = count
315
+ self._currCounts[1] = count
316
+ if cNovel is not None:
317
+ count = checkInt(cNovel, 0)
318
+ self._initCounts[2] = count
319
+ self._currCounts[2] = count
320
+ if cNotes is not None:
321
+ count = checkInt(cNotes, 0)
322
+ self._initCounts[3] = count
323
+ self._currCounts[3] = count
312
324
  return
313
325
 
314
- def setCurrCounts(self, novel: Any = None, notes: Any = None) -> None:
315
- """Set the word count totals for novel and note files."""
316
- if novel is not None:
317
- self._currCounts[0] = checkInt(novel, 0)
318
- if notes is not None:
319
- self._currCounts[1] = checkInt(notes, 0)
326
+ def setCurrCounts(
327
+ self, wNovel: Any = None, wNotes: Any = None, cNovel: Any = None, cNotes: Any = None
328
+ ) -> None:
329
+ """Set the count totals for novel and note files."""
330
+ if wNovel is not None:
331
+ self._currCounts[0] = checkInt(wNovel, 0)
332
+ if wNotes is not None:
333
+ self._currCounts[1] = checkInt(wNotes, 0)
334
+ if cNovel is not None:
335
+ self._currCounts[2] = checkInt(cNovel, 0)
336
+ if cNotes is not None:
337
+ self._currCounts[3] = checkInt(cNotes, 0)
320
338
  return
321
339
 
322
340
  def setAutoReplace(self, value: dict) -> None:
@@ -46,7 +46,7 @@ if TYPE_CHECKING:
46
46
  logger = logging.getLogger(__name__)
47
47
 
48
48
  FILE_VERSION = "1.5" # The current project file format version
49
- FILE_REVISION = "4" # The current project file format revision
49
+ FILE_REVISION = "5" # The current project file format revision
50
50
  HEX_VERSION = 0x0105
51
51
 
52
52
  NUM_VERSION = {
@@ -109,6 +109,8 @@ class ProjectXMLReader:
109
109
  Rev 3: Added TEMPLATE class. 2.3.
110
110
  Rev 4: Added shape attribute to status and importance entry
111
111
  nodes. 2.5.
112
+ Rev 5: Added novelChars and notesChars attributes to content
113
+ node. 2.7 RC 1.
112
114
  """
113
115
 
114
116
  def __init__(self, path: str | Path) -> None:
@@ -286,9 +288,9 @@ class ProjectXMLReader:
286
288
  elif xItem.tag == "spellLang": # Changed to spellChecking in 1.5
287
289
  data.setSpellLang(xItem.text)
288
290
  elif xItem.tag == "novelWordCount": # Moved to content attribute in 1.5
289
- data.setInitCounts(novel=xItem.text)
291
+ data.setInitCounts(wNovel=xItem.text)
290
292
  elif xItem.tag == "notesWordCount": # Moved to content attribute in 1.5
291
- data.setInitCounts(notes=xItem.text)
293
+ data.setInitCounts(wNotes=xItem.text)
292
294
 
293
295
  return
294
296
 
@@ -298,8 +300,13 @@ class ProjectXMLReader:
298
300
  """Parse the content section of the XML file."""
299
301
  logger.debug("Parsing <content> section")
300
302
 
301
- data.setInitCounts(novel=xSection.attrib.get("novelWords", None)) # Moved in 1.5
302
- data.setInitCounts(notes=xSection.attrib.get("notesWords", None)) # Moved in 1.5
303
+ # Moved in 1.5
304
+ data.setInitCounts(
305
+ wNovel=xSection.attrib.get("novelWords", None),
306
+ wNotes=xSection.attrib.get("notesWords", None),
307
+ cNovel=xSection.attrib.get("novelChars", None),
308
+ cNotes=xSection.attrib.get("notesChars", None),
309
+ )
303
310
 
304
311
  for xItem in xSection:
305
312
  if xItem.tag != "item":
@@ -527,10 +534,13 @@ class ProjectXMLWriter:
527
534
  self._packSingleValue(xImport, "entry", label, attrib=attrib)
528
535
 
529
536
  # Save Tree Content
537
+ counts = data.currCounts
530
538
  contAttr = {
531
539
  "items": str(len(content)),
532
- "novelWords": str(data.currCounts[0]),
533
- "notesWords": str(data.currCounts[1]),
540
+ "novelWords": str(counts[0]),
541
+ "notesWords": str(counts[1]),
542
+ "novelChars": str(counts[2]),
543
+ "notesChars": str(counts[3]),
534
544
  }
535
545
 
536
546
  xContent = ET.SubElement(xRoot, "content", attrib=contAttr)
@@ -79,29 +79,36 @@ class NWSessionLog:
79
79
  return False
80
80
 
81
81
  now = time()
82
- iNovel, iNotes = self._project.data.initCounts
83
- cNovel, cNotes = self._project.data.currCounts
84
- iTotal = iNovel + iNotes
85
- wDiff = cNovel + cNotes - iTotal
82
+ iWNovel, iWNotes, iCNovel, iCNotes = self._project.data.initCounts
83
+ cWNovel, cWNotes, cCNovel, cCNotes = self._project.data.currCounts
84
+ iWTotal = iWNovel + iWNotes
85
+ iCTotal = iCNovel + iCNotes
86
+ wDiff = cWNovel + cWNotes - iWTotal
87
+ cDiff = cCNovel + cCNotes - iCTotal
86
88
  sTime = now - self._start
87
89
 
88
- logger.info("The session lasted %d sec and added %d words", int(sTime), wDiff)
89
- if sTime < 300 and wDiff == 0:
90
+ logger.info(
91
+ "The session lasted %d sec and added %d words abd %d characters",
92
+ int(sTime), wDiff, cDiff
93
+ )
94
+ if sTime < 300 and (wDiff == 0 or cDiff == 0):
90
95
  logger.info("Session too short, skipping log entry")
91
96
  return False
92
97
 
93
98
  try:
94
99
  if not sessFile.exists():
95
100
  with open(sessFile, mode="w", encoding="utf-8") as fObj:
96
- fObj.write(self.createInitial(iTotal))
101
+ fObj.write(self.createInitial(iWTotal))
97
102
 
98
103
  with open(sessFile, mode="a+", encoding="utf-8") as fObj:
99
104
  fObj.write(self.createRecord(
100
105
  start=formatTimeStamp(self._start),
101
106
  end=formatTimeStamp(now),
102
- novel=cNovel,
103
- notes=cNotes,
104
- idle=round(idleTime)
107
+ novel=cWNovel,
108
+ notes=cWNotes,
109
+ idle=round(idleTime),
110
+ cnovel=cCNovel,
111
+ cnotes=cCNotes,
105
112
  ))
106
113
 
107
114
  except Exception:
@@ -129,10 +136,19 @@ class NWSessionLog:
129
136
  data = json.dumps({"type": "initial", "offset": total})
130
137
  return f"{data}\n"
131
138
 
132
- def createRecord(self, start: str, end: str, novel: int, notes: int, idle: int) -> str:
139
+ def createRecord(
140
+ self, start: str, end: str, novel: int, notes: int, idle: int,
141
+ cnovel: int = 0, cnotes: int = 0,
142
+ ) -> str:
133
143
  """Low level function to create a log record."""
134
144
  data = json.dumps({
135
- "type": "record", "start": start, "end": end,
136
- "novel": novel, "notes": notes, "idle": idle,
145
+ "type": "record",
146
+ "start": start,
147
+ "end": end,
148
+ "novel": novel,
149
+ "notes": notes,
150
+ "cnovel": cnovel,
151
+ "cnotes": cnotes,
152
+ "idle": idle,
137
153
  })
138
154
  return f"{data}\n"
novelwriter/core/tree.py CHANGED
@@ -410,16 +410,20 @@ class NWTree:
410
410
 
411
411
  return True
412
412
 
413
- def sumWords(self) -> tuple[int, int]:
414
- """Loop over all entries and add up the word counts."""
415
- noteWords = 0
413
+ def sumCounts(self) -> tuple[int, int, int, int]:
414
+ """Loop over all entries and add up the word and char counts."""
416
415
  novelWords = 0
416
+ notesWords = 0
417
+ novelChars = 0
418
+ notesChars = 0
417
419
  for item in self._items.values():
418
420
  if item.itemLayout == nwItemLayout.NOTE:
419
- noteWords += item.wordCount
421
+ notesWords += item.wordCount
422
+ notesChars += item.charCount
420
423
  elif item.itemLayout == nwItemLayout.DOCUMENT:
421
424
  novelWords += item.wordCount
422
- return novelWords, noteWords
425
+ novelChars += item.charCount
426
+ return novelWords, notesWords, novelChars, notesChars
423
427
 
424
428
  ##
425
429
  # Tree Item Methods
@@ -73,8 +73,9 @@ class GuiDocMerge(NDialog):
73
73
  self.listBox.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
74
74
 
75
75
  # Merge Options
76
- self.trashLabel = QLabel(self.tr("Move merged items to Trash"), self)
77
76
  self.trashSwitch = NSwitch(self, height=iPx)
77
+ self.trashLabel = QLabel(self.tr("Move merged items to Trash"), self)
78
+ self.trashLabel.setBuddy(self.trashSwitch)
78
79
 
79
80
  self.optBox = QGridLayout()
80
81
  self.optBox.addWidget(self.trashLabel, 0, 0)
@@ -90,16 +90,19 @@ class GuiDocSplit(NDialog):
90
90
  self.splitLevel.currentIndexChanged.connect(self._reloadList)
91
91
 
92
92
  # Split Options
93
- self.folderLabel = QLabel(self.tr("Split into a new folder"), self)
94
93
  self.folderSwitch = NSwitch(self, height=iPx)
95
94
  self.folderSwitch.setChecked(intoFolder)
95
+ self.folderLabel = QLabel(self.tr("Split into a new folder"), self)
96
+ self.folderLabel.setBuddy(self.folderSwitch)
96
97
 
97
- self.hierarchyLabel = QLabel(self.tr("Create document hierarchy"), self)
98
98
  self.hierarchySwitch = NSwitch(self, height=iPx)
99
99
  self.hierarchySwitch.setChecked(docHierarchy)
100
+ self.hierarchyLabel = QLabel(self.tr("Create document hierarchy"), self)
101
+ self.hierarchyLabel.setBuddy(self.hierarchySwitch)
100
102
 
101
- self.trashLabel = QLabel(self.tr("Move split document to Trash"), self)
102
103
  self.trashSwitch = NSwitch(self, height=iPx)
104
+ self.trashLabel = QLabel(self.tr("Move split document to Trash"), self)
105
+ self.trashLabel.setBuddy(self.trashSwitch)
103
106
 
104
107
  self.optBox = QGridLayout()
105
108
  self.optBox.addWidget(self.folderLabel, 0, 0)
@@ -43,11 +43,14 @@ class GuiEditLabel(NDialog):
43
43
  self.setWindowTitle(self.tr("Item Label"))
44
44
 
45
45
  # Item Label
46
- self.labelValue = QLineEdit(self)
47
- self.labelValue.setMinimumWidth(220)
48
- self.labelValue.setMaxLength(200)
49
- self.labelValue.setText(text)
50
- self.labelValue.selectAll()
46
+ self.edtValue = QLineEdit(self)
47
+ self.edtValue.setMinimumWidth(220)
48
+ self.edtValue.setMaxLength(200)
49
+ self.edtValue.setText(text)
50
+ self.edtValue.selectAll()
51
+
52
+ self.lblValue = QLabel(self.tr("Label"), self)
53
+ self.lblValue.setBuddy(self.lblValue)
51
54
 
52
55
  # Buttons
53
56
  self.buttonBox = QDialogButtonBox(QtDialogOk | QtDialogCancel, self)
@@ -56,8 +59,8 @@ class GuiEditLabel(NDialog):
56
59
 
57
60
  # Assemble
58
61
  self.innerBox = QHBoxLayout()
59
- self.innerBox.addWidget(QLabel(self.tr("Label"), self), 0)
60
- self.innerBox.addWidget(self.labelValue, 1)
62
+ self.innerBox.addWidget(self.lblValue, 0)
63
+ self.innerBox.addWidget(self.edtValue, 1)
61
64
  self.innerBox.setSpacing(12)
62
65
 
63
66
  self.outerBox = QVBoxLayout()
@@ -77,7 +80,7 @@ class GuiEditLabel(NDialog):
77
80
 
78
81
  @property
79
82
  def itemLabel(self) -> str:
80
- return self.labelValue.text()
83
+ return self.edtValue.text()
81
84
 
82
85
  @classmethod
83
86
  def getLabel(cls, parent: QWidget, text: str) -> tuple[str, bool]:
@@ -34,9 +34,9 @@ from PyQt6.QtWidgets import (
34
34
  )
35
35
 
36
36
  from novelwriter import CONFIG, SHARED
37
- from novelwriter.common import compact, describeFont, uniqueCompact
37
+ from novelwriter.common import compact, describeFont, processDialogSymbols, uniqueCompact
38
38
  from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX, DEF_TREECOL
39
- from novelwriter.constants import nwLabels, nwUnicode, trConst
39
+ from novelwriter.constants import nwLabels, nwQuotes, nwUnicode, trConst
40
40
  from novelwriter.dialogs.quotes import GuiQuoteSelect
41
41
  from novelwriter.extensions.configlayout import NColorLabel, NScrollableForm
42
42
  from novelwriter.extensions.modified import (
@@ -80,6 +80,7 @@ class GuiPreferences(NDialog):
80
80
  # SideBar
81
81
  self.sidebar = NPagedSideBar(self)
82
82
  self.sidebar.setLabelColor(SHARED.theme.helpText)
83
+ self.sidebar.setAccessibleName(self.titleLabel.text())
83
84
  self.sidebar.buttonClicked.connect(self._sidebarClicked)
84
85
 
85
86
  # Form
@@ -225,6 +226,14 @@ class GuiPreferences(NDialog):
225
226
  self.tr("Turn off to use the Qt font dialog, which may have more options.")
226
227
  )
227
228
 
229
+ # Use Character Count
230
+ self.useCharCount = NSwitch(self)
231
+ self.useCharCount.setChecked(CONFIG.useCharCount)
232
+ self.mainForm.addRow(
233
+ self.tr("Prefer character count over word count"), self.useCharCount,
234
+ self.tr("Display character count instead where available.")
235
+ )
236
+
228
237
  # Document Style
229
238
  # ==============
230
239
 
@@ -636,21 +645,20 @@ class GuiPreferences(NDialog):
636
645
  self.tr("Lines starting with any of these symbols are dialogue.")
637
646
  )
638
647
 
639
- self.narratorBreak = QLineEdit(self)
640
- self.narratorBreak.setMaxLength(1)
641
- self.narratorBreak.setFixedWidth(boxFixed)
642
- self.narratorBreak.setAlignment(QtAlignCenter)
643
- self.narratorBreak.setText(CONFIG.narratorBreak)
648
+ self.narratorBreak = NComboBox(self)
649
+ self.narratorDialog = NComboBox(self)
650
+ for key, value in nwQuotes.DASHES.items():
651
+ label = trConst(value)
652
+ self.narratorBreak.addItem(label, key)
653
+ self.narratorDialog.addItem(label, key)
654
+
655
+ self.narratorBreak.setCurrentData(CONFIG.narratorBreak, "")
656
+ self.narratorDialog.setCurrentData(CONFIG.narratorDialog, "")
657
+
644
658
  self.mainForm.addRow(
645
659
  self.tr("Narrator break symbol"), self.narratorBreak,
646
660
  self.tr("Symbol to indicate a narrator break in dialogue.")
647
661
  )
648
-
649
- self.narratorDialog = QLineEdit(self)
650
- self.narratorDialog.setMaxLength(1)
651
- self.narratorDialog.setFixedWidth(boxFixed)
652
- self.narratorDialog.setAlignment(QtAlignCenter)
653
- self.narratorDialog.setText(CONFIG.narratorDialog)
654
662
  self.mainForm.addRow(
655
663
  self.tr("Alternating dialogue/narration symbol"), self.narratorDialog,
656
664
  self.tr("Alternates dialogue highlighting within any paragraph.")
@@ -957,21 +965,24 @@ class GuiPreferences(NDialog):
957
965
  refreshTree = False
958
966
 
959
967
  # Appearance
960
- guiLocale = self.guiLocale.currentData()
961
- guiTheme = self.guiTheme.currentData()
962
- iconTheme = self.iconTheme.currentData()
968
+ guiLocale = self.guiLocale.currentData()
969
+ guiTheme = self.guiTheme.currentData()
970
+ iconTheme = self.iconTheme.currentData()
971
+ useCharCount = self.useCharCount.isChecked()
963
972
 
964
973
  updateTheme |= CONFIG.guiTheme != guiTheme
965
974
  updateTheme |= CONFIG.iconTheme != iconTheme
966
975
  needsRestart |= CONFIG.guiLocale != guiLocale
967
976
  needsRestart |= CONFIG.guiFont != self._guiFont
968
-
969
- CONFIG.guiLocale = guiLocale
970
- CONFIG.guiTheme = guiTheme
971
- CONFIG.iconTheme = iconTheme
972
- CONFIG.hideVScroll = self.hideVScroll.isChecked()
973
- CONFIG.hideHScroll = self.hideHScroll.isChecked()
974
- CONFIG.nativeFont = self.nativeFont.isChecked()
977
+ refreshTree |= CONFIG.useCharCount != useCharCount
978
+
979
+ CONFIG.guiLocale = guiLocale
980
+ CONFIG.guiTheme = guiTheme
981
+ CONFIG.iconTheme = iconTheme
982
+ CONFIG.hideVScroll = self.hideVScroll.isChecked()
983
+ CONFIG.hideHScroll = self.hideHScroll.isChecked()
984
+ CONFIG.nativeFont = self.nativeFont.isChecked()
985
+ CONFIG.useCharCount = useCharCount
975
986
  CONFIG.setGuiFont(self._guiFont)
976
987
 
977
988
  # Document Style
@@ -1034,9 +1045,9 @@ class GuiPreferences(NDialog):
1034
1045
  # Text Highlighting
1035
1046
  dialogueStyle = self.dialogStyle.currentData()
1036
1047
  allowOpenDial = self.allowOpenDial.isChecked()
1037
- dialogueLine = uniqueCompact(self.dialogLine.text())
1038
- narratorBreak = self.narratorBreak.text().strip()
1039
- narratorDialog = self.narratorDialog.text().strip()
1048
+ dialogueLine = processDialogSymbols(self.dialogLine.text())
1049
+ narratorBreak = self.narratorBreak.currentData()
1050
+ narratorDialog = self.narratorDialog.currentData()
1040
1051
  altDialogOpen = compact(self.altDialogOpen.text())
1041
1052
  altDialogClose = compact(self.altDialogClose.text())
1042
1053
  highlightEmph = self.highlightEmph.isChecked()
@@ -86,6 +86,7 @@ class GuiProjectSettings(NDialog):
86
86
  # SideBar
87
87
  self.sidebar = NPagedSideBar(self)
88
88
  self.sidebar.setLabelColor(SHARED.theme.helpText)
89
+ self.sidebar.setAccessibleName(self.titleLabel.text())
89
90
  self.sidebar.addButton(self.tr("Settings"), self.PAGE_SETTINGS)
90
91
  self.sidebar.addButton(self.tr("Status"), self.PAGE_STATUS)
91
92
  self.sidebar.addButton(self.tr("Importance"), self.PAGE_IMPORT)
@@ -349,6 +350,7 @@ class _StatusPage(NFixedPage):
349
350
  self.listBox.setHeaderLabels([self.tr("Label"), self.tr("Usage")])
350
351
  self.listBox.setColumnWidth(self.C_LABEL, wCol0)
351
352
  self.listBox.setIndentation(0)
353
+ self.listBox.setAccessibleName(pageLabel)
352
354
  self.listBox.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
353
355
  self.listBox.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
354
356
  self.listBox.itemSelectionChanged.connect(self._onSelectionChanged)
@@ -686,6 +688,7 @@ class _ReplacePage(NFixedPage):
686
688
  self.listBox.setHeaderLabels([self.tr("Keyword"), self.tr("Replace With")])
687
689
  self.listBox.setColumnWidth(self.C_KEY, wCol0)
688
690
  self.listBox.setIndentation(0)
691
+ self.listBox.setAccessibleName(self.pageTitle.text())
689
692
  self.listBox.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
690
693
  self.listBox.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
691
694
  self.listBox.itemSelectionChanged.connect(self._onSelectionChanged)
@@ -211,7 +211,8 @@ class NScrollableForm(QScrollArea):
211
211
  else:
212
212
  qWidget = widget
213
213
 
214
- qLabel = QLabel(label or "", self)
214
+ text = label or ""
215
+ qLabel = QLabel(text, self)
215
216
  qLabel.setIndent(self._indent)
216
217
  qLabel.setBuddy(qWidget)
217
218
 
@@ -227,6 +228,7 @@ class NScrollableForm(QScrollArea):
227
228
  row.addLayout(labelBox, stretch[0])
228
229
  if editable:
229
230
  self._editable[editable] = qHelp
231
+ text = f"{text}: {helpText}"
230
232
  else:
231
233
  row.addWidget(qLabel, stretch[0])
232
234
 
@@ -235,6 +237,7 @@ class NScrollableForm(QScrollArea):
235
237
  box.addWidget(qWidget, 1)
236
238
  box.addWidget(QLabel(unit, self), 0)
237
239
  row.addLayout(box, stretch[1])
240
+ text = f"{text} Unit: {unit}"
238
241
  elif isinstance(button, QAbstractButton):
239
242
  box = QHBoxLayout()
240
243
  box.addWidget(qWidget, 1)
@@ -243,10 +246,11 @@ class NScrollableForm(QScrollArea):
243
246
  else:
244
247
  row.addWidget(qWidget, stretch[1])
245
248
 
249
+ self._first = False
246
250
  self._layout.addLayout(row)
247
251
  if label:
248
252
  self._index[label.strip()] = qWidget
249
- self._first = False
253
+ qLabel.setAccessibleName(text)
250
254
 
251
255
  return
252
256
 
@@ -23,12 +23,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
23
23
  """
24
24
  from __future__ import annotations
25
25
 
26
- from PyQt6.QtCore import QPropertyAnimation, Qt, pyqtProperty # pyright: ignore
27
- from PyQt6.QtGui import QEnterEvent, QMouseEvent, QPainter, QPaintEvent, QResizeEvent
26
+ from PyQt6.QtCore import QPropertyAnimation, Qt, pyqtProperty, pyqtSlot # pyright: ignore
27
+ from PyQt6.QtGui import QEnterEvent, QPainter, QPaintEvent, QResizeEvent
28
28
  from PyQt6.QtWidgets import QAbstractButton, QWidget
29
29
 
30
30
  from novelwriter import SHARED
31
- from novelwriter.types import QtMouseLeft, QtNoPen, QtPaintAntiAlias, QtSizeFixed
31
+ from novelwriter.types import QtNoPen, QtPaintAntiAlias, QtSizeFixed
32
32
 
33
33
 
34
34
  class NSwitch(QAbstractButton):
@@ -50,6 +50,8 @@ class NSwitch(QAbstractButton):
50
50
  self.setFixedHeight(self._xH)
51
51
  self._offset = self._xR
52
52
 
53
+ self.clicked.connect(self._onClick)
54
+
53
55
  return
54
56
 
55
57
  ##
@@ -94,7 +96,7 @@ class NSwitch(QAbstractButton):
94
96
  painter.setRenderHint(QtPaintAntiAlias, True)
95
97
  painter.setOpacity(1.0 if self.isEnabled() else 0.5)
96
98
 
97
- painter.setPen(palette.mid().color())
99
+ painter.setPen(palette.highlight().color() if self.hasFocus() else palette.mid().color())
98
100
  painter.setBrush(palette.highlight() if self.isChecked() else palette.alternateBase())
99
101
  painter.drawRoundedRect(0, 0, self._xW, self._xH, self._xR, self._xR)
100
102
 
@@ -106,19 +108,18 @@ class NSwitch(QAbstractButton):
106
108
 
107
109
  return
108
110
 
109
- def mouseReleaseEvent(self, event: QMouseEvent) -> None:
110
- """Animate the switch on mouse release."""
111
- super().mouseReleaseEvent(event)
112
- if event.button() == QtMouseLeft:
113
- anim = QPropertyAnimation(self, b"offset", self)
114
- anim.setDuration(120)
115
- anim.setStartValue(self._offset)
116
- anim.setEndValue((self._xW - self._xR) if self.isChecked() else self._xR)
117
- anim.start()
118
- return
119
-
120
111
  def enterEvent(self, event: QEnterEvent) -> None:
121
112
  """Change the cursor when hovering the button."""
122
113
  self.setCursor(Qt.CursorShape.PointingHandCursor)
123
114
  super().enterEvent(event)
124
115
  return
116
+
117
+ @pyqtSlot(bool)
118
+ def _onClick(self, checked: bool) -> None:
119
+ """Animate the toggle action."""
120
+ anim = QPropertyAnimation(self, b"offset", self)
121
+ anim.setDuration(120)
122
+ anim.setStartValue(self._offset)
123
+ anim.setEndValue((self._xW - self._xR) if checked else self._xR)
124
+ anim.start()
125
+ return
@@ -100,6 +100,7 @@ class NSwitchBox(QScrollArea):
100
100
  switch.toggled.connect(lambda state: self._emitSwitchSignal(identifier, state))
101
101
  self._content.addWidget(switch, self._index, 2, QtAlignRight)
102
102
 
103
+ label.setBuddy(switch)
103
104
  self._widgets.append(switch)
104
105
  self._bumpIndex()
105
106
 
@@ -614,7 +614,8 @@ class Tokenizer(ABC):
614
614
  tStyle |= BlockFmt.JUSTIFY
615
615
 
616
616
  if cStyle in (
617
- nwComment.SYNOPSIS, nwComment.SHORT, nwComment.PLAIN, nwComment.STORY
617
+ nwComment.SYNOPSIS, nwComment.SHORT, nwComment.PLAIN,
618
+ nwComment.STORY, nwComment.NOTE,
618
619
  ):
619
620
  bStyle = COMMENT_STYLE[cStyle]
620
621
  tLine, tFmt = self._formatComment(bStyle, cKey, cText)