From d6fcc89da3d8a74a109d2cade06c5b67c368faf6 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 4 May 2025 17:03:39 +0200
Subject: [PATCH 01/16] Explicitly convert date and datetime to QDate and
QDateTime (#2325)
---
novelwriter/config.py | 13 +++++++++----
tests/test_base/test_base_config.py | 7 +++++++
2 files changed, 16 insertions(+), 4 deletions(-)
diff --git a/novelwriter/config.py b/novelwriter/config.py
index 78a4b843..35690669 100644
--- a/novelwriter/config.py
+++ b/novelwriter/config.py
@@ -34,8 +34,8 @@ from time import time
from typing import TYPE_CHECKING, Final
from PyQt6.QtCore import (
- PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QLibraryInfo,
- QLocale, QStandardPaths, QSysInfo, QTranslator
+ PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QDate,
+ QDateTime, QLibraryInfo, QLocale, QStandardPaths, QSysInfo, QTranslator
)
from PyQt6.QtGui import QFont, QFontDatabase, QFontMetrics
from PyQt6.QtWidgets import QApplication
@@ -467,11 +467,16 @@ class Config:
def localDate(self, value: datetime) -> str:
"""Return a localised date format."""
- return self._dLocale.toString(value, self._dShortDate)
+ # Explicitly convert the date first, see bug #2325
+ return self._dLocale.toString(QDate(value.year, value.month, value.day), self._dShortDate)
def localDateTime(self, value: datetime) -> str:
"""Return a localised datetime format."""
- return self._dLocale.toString(value, self._dShortDateTime)
+ # Explicitly convert the datetime first, see bug #2325
+ return self._dLocale.toString(
+ QDateTime(value.year, value.month, value.day, value.hour, value.minute, value.second),
+ self._dShortDateTime,
+ )
def listLanguages(self, lngSet: int) -> list[tuple[str, str]]:
"""List localisation files in the i18n folder. The default GUI
diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py
index 74468629..be805df0 100644
--- a/tests/test_base/test_base_config.py
+++ b/tests/test_base/test_base_config.py
@@ -20,6 +20,7 @@ along with this program. If not, see .
"""
from __future__ import annotations
+import datetime
import json
import sys
@@ -189,6 +190,12 @@ def testBaseConfig_Localisation(fncPath, tstPaths):
languages = tstConf.listLanguages(tstConf.LANG_NW)
assert languages == [("en_GB", "British English"), ("fr", "Français")]
+ # Date Formats
+ # Checks for bug #2325
+ assert CONFIG._dLocale.bcp47Name() == "en-GB"
+ assert CONFIG.localDate(datetime.datetime.fromtimestamp(1746370775)) == "04/05/2025"
+ assert CONFIG.localDateTime(datetime.datetime.fromtimestamp(1746370775)) == "04/05/2025 16:59"
+
@pytest.mark.base
def testBaseConfig_Methods(fncPath):
From ae5e90be020446ea650139b624d84eda2968283e Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 4 May 2025 17:24:35 +0200
Subject: [PATCH 02/16] Lock the date and time format in the config test, and
make it time zone ignorant
---
tests/test_base/test_base_config.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py
index be805df0..ee43b86a 100644
--- a/tests/test_base/test_base_config.py
+++ b/tests/test_base/test_base_config.py
@@ -192,9 +192,11 @@ def testBaseConfig_Localisation(fncPath, tstPaths):
# Date Formats
# Checks for bug #2325
- assert CONFIG._dLocale.bcp47Name() == "en-GB"
- assert CONFIG.localDate(datetime.datetime.fromtimestamp(1746370775)) == "04/05/2025"
- assert CONFIG.localDateTime(datetime.datetime.fromtimestamp(1746370775)) == "04/05/2025 16:59"
+ ts = datetime.datetime.fromtimestamp(1746370775)
+ CONFIG._dShortDate = "dd/MM/yyyy"
+ CONFIG._dShortDateTime = "dd/MM/yyyy HH:mm"
+ assert CONFIG.localDate(ts) == ts.strftime("%d/%m/%Y")
+ assert CONFIG.localDateTime(ts) == ts.strftime("%d/%m/%Y %H:%M")
@pytest.mark.base
From 6f3886797289470d87df0158e92b27c9fb7f09df Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 4 May 2025 21:53:49 +0200
Subject: [PATCH 03/16] Limit narrator symbols to dashes (#2324)
---
novelwriter/config.py | 9 +++++---
novelwriter/constants.py | 7 ++++++
novelwriter/dialogs/preferences.py | 27 +++++++++++-----------
tests/test_dialogs/test_dlg_preferences.py | 4 ++--
4 files changed, 28 insertions(+), 19 deletions(-)
diff --git a/novelwriter/config.py b/novelwriter/config.py
index 35690669..0597c82e 100644
--- a/novelwriter/config.py
+++ b/novelwriter/config.py
@@ -44,7 +44,7 @@ from novelwriter.common import (
NWConfigParser, checkInt, checkPath, describeFont, fontMatcher,
formatTimeStamp
)
-from novelwriter.constants import nwFiles, nwHtmlUnicode, nwUnicode
+from novelwriter.constants import nwFiles, nwHtmlUnicode, nwQuotes, nwUnicode
from novelwriter.error import formatException, logException
if TYPE_CHECKING:
@@ -681,8 +681,8 @@ class Config:
self.dialogStyle = conf.rdInt(sec, "dialogstyle", self.dialogStyle)
self.allowOpenDial = conf.rdBool(sec, "allowopendial", self.allowOpenDial)
self.dialogLine = conf.rdStr(sec, "dialogline", self.dialogLine)
- self.narratorBreak = conf.rdStr(sec, "narratorbreak", self.narratorBreak)
- self.narratorDialog = conf.rdStr(sec, "narratordialog", self.narratorDialog)
+ narratorBreak = conf.rdStr(sec, "narratorbreak", self.narratorBreak)
+ narratorDialog = conf.rdStr(sec, "narratordialog", self.narratorDialog)
self.altDialogOpen = conf.rdStr(sec, "altdialogopen", self.altDialogOpen)
self.altDialogClose = conf.rdStr(sec, "altdialogclose", self.altDialogClose)
self.highlightEmph = conf.rdBool(sec, "highlightemph", self.highlightEmph)
@@ -721,6 +721,9 @@ class Config:
logger.info("Using straight double quotes, so disabling auto-replace")
self.doReplaceDQuote = False
+ self.narratorBreak = narratorBreak if narratorBreak in nwQuotes.DASHES else ""
+ self.narratorDialog = narratorDialog if narratorDialog in nwQuotes.DASHES else ""
+
return True
def saveConfig(self) -> bool:
diff --git a/novelwriter/constants.py b/novelwriter/constants.py
index 9928ece4..72ba4b90 100644
--- a/novelwriter/constants.py
+++ b/novelwriter/constants.py
@@ -505,6 +505,13 @@ class nwQuotes:
"\u300f": QT_TRANSLATE_NOOP("Constant", "Right white corner bracket"),
}
+ DASHES: Final[dict[str, str]] = {
+ "": QT_TRANSLATE_NOOP("Constant", "None"),
+ "\u2013": QT_TRANSLATE_NOOP("Constant", "Short dash"),
+ "\u2014": QT_TRANSLATE_NOOP("Constant", "Long dash"),
+ "\u2015": QT_TRANSLATE_NOOP("Constant", "Horizontal bar"),
+ }
+
class nwUnicode:
"""Supported unicode character constants and their HTML equivalents."""
diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py
index aa4f9cd1..166b98be 100644
--- a/novelwriter/dialogs/preferences.py
+++ b/novelwriter/dialogs/preferences.py
@@ -36,7 +36,7 @@ from PyQt6.QtWidgets import (
from novelwriter import CONFIG, SHARED
from novelwriter.common import compact, describeFont, uniqueCompact
from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX, DEF_TREECOL
-from novelwriter.constants import nwLabels, nwUnicode, trConst
+from novelwriter.constants import nwLabels, nwQuotes, nwUnicode, trConst
from novelwriter.dialogs.quotes import GuiQuoteSelect
from novelwriter.extensions.configlayout import NColorLabel, NScrollableForm
from novelwriter.extensions.modified import (
@@ -636,21 +636,20 @@ class GuiPreferences(NDialog):
self.tr("Lines starting with any of these symbols are dialogue.")
)
- self.narratorBreak = QLineEdit(self)
- self.narratorBreak.setMaxLength(1)
- self.narratorBreak.setFixedWidth(boxFixed)
- self.narratorBreak.setAlignment(QtAlignCenter)
- self.narratorBreak.setText(CONFIG.narratorBreak)
+ self.narratorBreak = NComboBox(self)
+ self.narratorDialog = NComboBox(self)
+ for key, value in nwQuotes.DASHES.items():
+ label = trConst(value)
+ self.narratorBreak.addItem(label, key)
+ self.narratorDialog.addItem(label, key)
+
+ self.narratorBreak.setCurrentData(CONFIG.narratorBreak, "")
+ self.narratorDialog.setCurrentData(CONFIG.narratorDialog, "")
+
self.mainForm.addRow(
self.tr("Narrator break symbol"), self.narratorBreak,
self.tr("Symbol to indicate a narrator break in dialogue.")
)
-
- self.narratorDialog = QLineEdit(self)
- self.narratorDialog.setMaxLength(1)
- self.narratorDialog.setFixedWidth(boxFixed)
- self.narratorDialog.setAlignment(QtAlignCenter)
- self.narratorDialog.setText(CONFIG.narratorDialog)
self.mainForm.addRow(
self.tr("Alternating dialogue/narration symbol"), self.narratorDialog,
self.tr("Alternates dialogue highlighting within any paragraph.")
@@ -1035,8 +1034,8 @@ class GuiPreferences(NDialog):
dialogueStyle = self.dialogStyle.currentData()
allowOpenDial = self.allowOpenDial.isChecked()
dialogueLine = uniqueCompact(self.dialogLine.text())
- narratorBreak = self.narratorBreak.text().strip()
- narratorDialog = self.narratorDialog.text().strip()
+ narratorBreak = self.narratorBreak.currentData()
+ narratorDialog = self.narratorDialog.currentData()
altDialogOpen = compact(self.altDialogOpen.text())
altDialogClose = compact(self.altDialogClose.text())
highlightEmph = self.highlightEmph.isChecked()
diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py
index c7a1b7e4..5cb97dd0 100644
--- a/tests/test_dialogs/test_dlg_preferences.py
+++ b/tests/test_dialogs/test_dlg_preferences.py
@@ -265,8 +265,8 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
prefs.dialogStyle.setCurrentData(3, 0)
prefs.allowOpenDial.setChecked(False)
prefs.dialogLine.setText("–")
- prefs.narratorBreak.setText("–")
- prefs.narratorDialog.setText("–")
+ prefs.narratorBreak.setCurrentData("–", "")
+ prefs.narratorDialog.setCurrentData("–", "")
prefs.altDialogOpen.setText("<")
prefs.altDialogClose.setText(">")
prefs.highlightEmph.setChecked(False)
From 607001f2dd82ebb214ea801f93ee789f9adb4a58 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 4 May 2025 22:17:13 +0200
Subject: [PATCH 04/16] Restrict dialogue line symbols
---
novelwriter/common.py | 11 ++++++++++-
novelwriter/config.py | 5 +++--
novelwriter/constants.py | 6 ++++++
novelwriter/dialogs/preferences.py | 4 ++--
tests/test_base/test_base_common.py | 13 +++++++++++--
5 files changed, 32 insertions(+), 7 deletions(-)
diff --git a/novelwriter/common.py b/novelwriter/common.py
index 7cb6c5e0..a4e37f62 100644
--- a/novelwriter/common.py
+++ b/novelwriter/common.py
@@ -40,7 +40,7 @@ from PyQt6.QtCore import QCoreApplication, QMimeData, QUrl
from PyQt6.QtGui import QAction, QDesktopServices, QFont, QFontDatabase, QFontInfo
from PyQt6.QtWidgets import QMenu, QMenuBar, QWidget
-from novelwriter.constants import nwConst, nwLabels, nwUnicode, trConst
+from novelwriter.constants import nwConst, nwLabels, nwQuotes, nwUnicode, trConst
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException
@@ -298,6 +298,15 @@ def uniqueCompact(text: str) -> str:
return "".join(sorted(set(compact(text))))
+def processDialogSymbols(symbols: str) -> str:
+ """Process dialogue line symbols."""
+ result = ""
+ for c in uniqueCompact(symbols):
+ if c in nwQuotes.ALLOWED:
+ result += c
+ return result
+
+
def elide(text: str, length: int) -> str:
"""Elide a piece of text to a maximum length."""
if len(text) > (cut := max(4, length)):
diff --git a/novelwriter/config.py b/novelwriter/config.py
index 0597c82e..d820c66a 100644
--- a/novelwriter/config.py
+++ b/novelwriter/config.py
@@ -42,7 +42,7 @@ from PyQt6.QtWidgets import QApplication
from novelwriter.common import (
NWConfigParser, checkInt, checkPath, describeFont, fontMatcher,
- formatTimeStamp
+ formatTimeStamp, processDialogSymbols
)
from novelwriter.constants import nwFiles, nwHtmlUnicode, nwQuotes, nwUnicode
from novelwriter.error import formatException, logException
@@ -680,7 +680,7 @@ class Config:
self.showFullPath = conf.rdBool(sec, "showfullpath", self.showFullPath)
self.dialogStyle = conf.rdInt(sec, "dialogstyle", self.dialogStyle)
self.allowOpenDial = conf.rdBool(sec, "allowopendial", self.allowOpenDial)
- self.dialogLine = conf.rdStr(sec, "dialogline", self.dialogLine)
+ dialogLine = conf.rdStr(sec, "dialogline", self.dialogLine)
narratorBreak = conf.rdStr(sec, "narratorbreak", self.narratorBreak)
narratorDialog = conf.rdStr(sec, "narratordialog", self.narratorDialog)
self.altDialogOpen = conf.rdStr(sec, "altdialogopen", self.altDialogOpen)
@@ -721,6 +721,7 @@ class Config:
logger.info("Using straight double quotes, so disabling auto-replace")
self.doReplaceDQuote = False
+ self.dialogLine = processDialogSymbols(dialogLine)
self.narratorBreak = narratorBreak if narratorBreak in nwQuotes.DASHES else ""
self.narratorDialog = narratorDialog if narratorDialog in nwQuotes.DASHES else ""
diff --git a/novelwriter/constants.py b/novelwriter/constants.py
index 72ba4b90..16c5a082 100644
--- a/novelwriter/constants.py
+++ b/novelwriter/constants.py
@@ -512,6 +512,12 @@ class nwQuotes:
"\u2015": QT_TRANSLATE_NOOP("Constant", "Horizontal bar"),
}
+ ALLOWED: Final[list[str]] = [
+ "\u0027", "\u0022", "\u2018", "\u2019", "\u201a", "\u201b", "\u201c", "\u201d", "\u201e",
+ "\u201f", "\u2e42", "\u2039", "\u203a", "\u00ab", "\u00bb", "\u300c", "\u300d", "\u300e",
+ "\u300f", "\u2013", "\u2014", "\u2015",
+ ]
+
class nwUnicode:
"""Supported unicode character constants and their HTML equivalents."""
diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py
index 166b98be..f98374af 100644
--- a/novelwriter/dialogs/preferences.py
+++ b/novelwriter/dialogs/preferences.py
@@ -34,7 +34,7 @@ from PyQt6.QtWidgets import (
)
from novelwriter import CONFIG, SHARED
-from novelwriter.common import compact, describeFont, uniqueCompact
+from novelwriter.common import compact, describeFont, processDialogSymbols, uniqueCompact
from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX, DEF_TREECOL
from novelwriter.constants import nwLabels, nwQuotes, nwUnicode, trConst
from novelwriter.dialogs.quotes import GuiQuoteSelect
@@ -1033,7 +1033,7 @@ class GuiPreferences(NDialog):
# Text Highlighting
dialogueStyle = self.dialogStyle.currentData()
allowOpenDial = self.allowOpenDial.isChecked()
- dialogueLine = uniqueCompact(self.dialogLine.text())
+ dialogueLine = processDialogSymbols(self.dialogLine.text())
narratorBreak = self.narratorBreak.currentData()
narratorDialog = self.narratorDialog.currentData()
altDialogOpen = compact(self.altDialogOpen.text())
diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py
index bfd2b55c..006daa42 100644
--- a/tests/test_base/test_base_common.py
+++ b/tests/test_base/test_base_common.py
@@ -37,8 +37,9 @@ from novelwriter.common import (
formatFileFilter, formatInt, formatTime, formatTimeStamp, formatVersion,
fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, isItemLayout,
isItemType, isListInstance, isTitleTag, jsonEncode, makeFileNameSafe,
- minmax, numberToRoman, openExternalPath, readTextFile, simplified,
- transferCase, uniqueCompact, xmlElement, xmlIndent, xmlSubElem, yesNo
+ minmax, numberToRoman, openExternalPath, processDialogSymbols,
+ readTextFile, simplified, transferCase, uniqueCompact, xmlElement,
+ xmlIndent, xmlSubElem, yesNo
)
from tests.mocked import causeOSError
@@ -377,6 +378,14 @@ def testBaseCommon_uniqueCompact():
assert uniqueCompact("3 2 1") == "123"
+@pytest.mark.base
+def testBaseCommon_processDialogSymbols():
+ """Test the processDialogSymbols function."""
+ assert processDialogSymbols("abc") == ""
+ assert processDialogSymbols("\u00ab\u00ab\u00bb\u00bb") == "\u00ab\u00bb"
+ assert processDialogSymbols("-\u2013\u2014\u2015") == "\u2013\u2014\u2015"
+
+
@pytest.mark.base
def testBaseCommon_elide():
"""Test the elide function."""
From 307965a46450c39d8d15c821bc3b36e94e460f50 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 8 May 2025 21:15:55 +0200
Subject: [PATCH 05/16] Add tests using the Qt model tester
---
tests/test_core/test_core_itemmodel.py | 11 ++++++++++-
tests/test_core/test_core_novelmodel.py | 8 ++++++++
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/tests/test_core/test_core_itemmodel.py b/tests/test_core/test_core_itemmodel.py
index c67f9197..d6efd718 100644
--- a/tests/test_core/test_core_itemmodel.py
+++ b/tests/test_core/test_core_itemmodel.py
@@ -23,11 +23,12 @@ from __future__ import annotations
import pytest
from PyQt6.QtCore import QMimeData, QModelIndex, Qt
+from PyQt6.QtTest import QAbstractItemModelTester
from novelwriter.common import decodeMimeHandles
from novelwriter.constants import nwConst
from novelwriter.core.item import NWItem
-from novelwriter.core.itemmodel import INV_ROOT, NODE_FLAGS, ProjectNode
+from novelwriter.core.itemmodel import INV_ROOT, NODE_FLAGS, ProjectModel, ProjectNode
from novelwriter.core.project import NWProject
from novelwriter.enum import nwItemLayout, nwItemType
@@ -253,6 +254,14 @@ def testCoreItemModel_ProjectNode_Data(mockGUI, mockRnd, fncPath):
assert scene.flags() == NODE_FLAGS | Qt.ItemFlag.ItemIsDragEnabled
+@pytest.mark.core
+def testCoreItemModel_ProjectModel_ModelTest(mockGUI):
+ """Run the Qt model tester on the model."""
+ project = NWProject()
+ model = ProjectModel(project.tree)
+ QAbstractItemModelTester(model)
+
+
@pytest.mark.core
def testCoreItemModel_ProjectModel_Interface(mockGUI, mockRnd, fncPath):
"""Test the model interface for the project model."""
diff --git a/tests/test_core/test_core_novelmodel.py b/tests/test_core/test_core_novelmodel.py
index b042f5e6..22955c70 100644
--- a/tests/test_core/test_core_novelmodel.py
+++ b/tests/test_core/test_core_novelmodel.py
@@ -23,6 +23,7 @@ from __future__ import annotations
import pytest
from PyQt6.QtCore import QModelIndex, Qt
+from PyQt6.QtTest import QAbstractItemModelTester
from novelwriter.core.indexdata import IndexHeading
from novelwriter.core.novelmodel import NovelModel
@@ -32,6 +33,13 @@ from novelwriter.enum import nwNovelExtra
from tests.tools import C, buildTestProject
+@pytest.mark.core
+def testCoreNovelModel_ModelTest(nwGUI):
+ """Run the Qt model tester on the model."""
+ model = NovelModel()
+ QAbstractItemModelTester(model)
+
+
@pytest.mark.core
def testCoreNovelModel_Interface(nwGUI, fncPath, mockRnd):
"""Test the novel model interface."""
From 39c072050b56c48d47a6504d5311b4f54327d939 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 11 May 2025 12:50:00 +0200
Subject: [PATCH 06/16] Add shapes to default status labels (#2332)
---
novelwriter/core/project.py | 16 +++++-----
.../coreProject_NewFileFolder_nwProject.nwx | 18 +++++------
.../coreProject_NewRoot_nwProject.nwx | 18 +++++------
.../coreTools_DocDuplicator_nwProject.nwx | 18 +++++------
.../coreTools_ProjectBuilderA_nwProject.nwx | 18 +++++------
.../coreTools_ProjectBuilderB_nwProject.nwx | 18 +++++------
.../guiEditor_Main_Final_nwProject.nwx | 20 ++++++-------
.../guiEditor_Main_Initial_nwProject.nwx | 18 +++++------
.../test_dialogs/test_dlg_projectsettings.py | 30 +++++++++----------
9 files changed, 87 insertions(+), 87 deletions(-)
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 9ad46682..fb71e9ac 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -483,14 +483,14 @@ class NWProject:
def setDefaultStatusImport(self) -> None:
"""Set the default status and importance values."""
- self._data.itemStatus.add(None, self.tr("New"), (100, 100, 100), "SQUARE", 0)
- self._data.itemStatus.add(None, self.tr("Note"), (200, 50, 0), "SQUARE", 0)
- self._data.itemStatus.add(None, self.tr("Draft"), (200, 150, 0), "SQUARE", 0)
- self._data.itemStatus.add(None, self.tr("Finished"), (50, 200, 0), "SQUARE", 0)
- self._data.itemImport.add(None, self.tr("New"), (100, 100, 100), "SQUARE", 0)
- self._data.itemImport.add(None, self.tr("Minor"), (200, 50, 0), "SQUARE", 0)
- self._data.itemImport.add(None, self.tr("Major"), (200, 150, 0), "SQUARE", 0)
- self._data.itemImport.add(None, self.tr("Main"), (50, 200, 0), "SQUARE", 0)
+ self._data.itemStatus.add(None, self.tr("New"), (120, 120, 120), "STAR", 0)
+ self._data.itemStatus.add(None, self.tr("Note"), (205, 171, 143), "TRIANGLE", 0)
+ self._data.itemStatus.add(None, self.tr("Draft"), (143, 240, 164), "CIRCLE_T", 0)
+ self._data.itemStatus.add(None, self.tr("Finished"), (249, 240, 107), "STAR", 0)
+ self._data.itemImport.add(None, self.tr("New"), (120, 120, 120), "SQUARE", 0)
+ self._data.itemImport.add(None, self.tr("Minor"), (220, 138, 221), "BLOCK_2", 0)
+ self._data.itemImport.add(None, self.tr("Major"), (220, 138, 221), "BLOCK_3", 0)
+ self._data.itemImport.add(None, self.tr("Main"), (220, 138, 221), "BLOCK_4", 0)
return
def setProjectLang(self, language: str | None) -> None:
diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
index 80142d13..6b5adc10 100644
--- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx
+++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
Jane Doe
@@ -16,16 +16,16 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx
index 5d583362..bb72469a 100644
--- a/tests/reference/coreProject_NewRoot_nwProject.nwx
+++ b/tests/reference/coreProject_NewRoot_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
Jane Doe
@@ -16,16 +16,16 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
diff --git a/tests/reference/coreTools_DocDuplicator_nwProject.nwx b/tests/reference/coreTools_DocDuplicator_nwProject.nwx
index 2f571386..1405e3e9 100644
--- a/tests/reference/coreTools_DocDuplicator_nwProject.nwx
+++ b/tests/reference/coreTools_DocDuplicator_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
Jane Doe
@@ -16,16 +16,16 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
diff --git a/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx b/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx
index a35421d0..fbe4eb4c 100644
--- a/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx
+++ b/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Test Project A
Jane Doe
@@ -16,16 +16,16 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
diff --git a/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx b/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx
index 0185bc6a..203e7fbf 100644
--- a/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx
+++ b/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Test Project B
Jane Doe
@@ -16,16 +16,16 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx
index e90fa531..ac0d929b 100644
--- a/tests/reference/guiEditor_Main_Final_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx
@@ -1,6 +1,6 @@
-
-
+
+
New Project
Jane Doe
@@ -16,16 +16,16 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
index 27e64dac..1c94d8c6 100644
--- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
Jane Doe
@@ -16,16 +16,16 @@
- New
- Note
- Draft
- Finished
+ New
+ Note
+ Draft
+ Finished
- New
- Minor
- Major
- Main
+ New
+ Minor
+ Major
+ Main
diff --git a/tests/test_dialogs/test_dlg_projectsettings.py b/tests/test_dialogs/test_dlg_projectsettings.py
index d28322d4..edd0cebe 100644
--- a/tests/test_dialogs/test_dlg_projectsettings.py
+++ b/tests/test_dialogs/test_dlg_projectsettings.py
@@ -214,18 +214,18 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRn
assert update[0][0] == C.sNew
assert update[0][1].name == "New"
- assert update[0][1].color == QColor(100, 100, 100)
- assert update[0][1].shape == nwStatusShape.SQUARE
+ assert update[0][1].color == QColor(120, 120, 120)
+ assert update[0][1].shape == nwStatusShape.STAR
assert update[1][0] == C.sDraft
assert update[1][1].name == "Draft"
- assert update[1][1].color == QColor(200, 150, 0)
- assert update[1][1].shape == nwStatusShape.SQUARE
+ assert update[1][1].color == QColor(143, 240, 164)
+ assert update[1][1].shape == nwStatusShape.CIRCLE_T
assert update[2][0] == C.sFinished
assert update[2][1].name == "Finished"
- assert update[2][1].color == QColor(50, 200, 0)
- assert update[2][1].shape == nwStatusShape.SQUARE
+ assert update[2][1].color == QColor(249, 240, 107)
+ assert update[2][1].shape == nwStatusShape.STAR
assert update[3][0] is None
assert update[3][1].name == "Final"
@@ -289,18 +289,18 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRn
assert update[0][0] == C.iNew
assert update[0][1].name == "New"
- assert update[0][1].color == QColor(100, 100, 100)
+ assert update[0][1].color == QColor(120, 120, 120)
assert update[0][1].shape == nwStatusShape.SQUARE
assert update[1][0] == C.iMajor
assert update[1][1].name == "Major"
- assert update[1][1].color == QColor(200, 150, 0)
- assert update[1][1].shape == nwStatusShape.SQUARE
+ assert update[1][1].color == QColor(220, 138, 221)
+ assert update[1][1].shape == nwStatusShape.BLOCK_3
assert update[2][0] == C.iMain
assert update[2][1].name == "Main"
- assert update[2][1].color == QColor(50, 200, 0)
- assert update[2][1].shape == nwStatusShape.SQUARE
+ assert update[2][1].color == QColor(220, 138, 221)
+ assert update[2][1].shape == nwStatusShape.BLOCK_4
assert update[3][0] is None
assert update[3][1].name == "Final"
@@ -354,10 +354,10 @@ def testDlgProjSettings_StatusImportExport(qtbot, monkeypatch, nwGUI, projPath,
assert expFile.is_file() is True
assert expFile.read_text().split() == [
- "SQUARE,#646464,New",
- "SQUARE,#c83200,Note",
- "SQUARE,#c89600,Draft",
- "SQUARE,#32c800,Finished",
+ "STAR,#787878,New",
+ "TRIANGLE,#cdab8f,Note",
+ "CIRCLE_T,#8ff0a4,Draft",
+ "STAR,#f9f06b,Finished",
]
# Import Error
From 699c27e3dc801b7c8609df3a78ebaec3ffb77bfc Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 11 May 2025 13:07:55 +0200
Subject: [PATCH 07/16] Improve new project creation, and generate new id for
sample projects
---
novelwriter/core/coretools.py | 53 ++++++++++---------
.../coreTools_ProjectBuilderA_Project.md | 6 +--
.../coreTools_ProjectBuilderB_Project.md | 6 +--
3 files changed, 35 insertions(+), 30 deletions(-)
diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py
index 815504e2..8c7b7195 100644
--- a/novelwriter/core/coretools.py
+++ b/novelwriter/core/coretools.py
@@ -416,13 +416,14 @@ class ProjectBuilder:
self._path = project.storage.storagePath
- lblNewProject = self.tr("New Project")
- lblTitlePage = self.tr("Title Page")
+ lblName = self.tr("New Project")
+ lblAuthor = self.tr("Author")
+ lblTitlePage = self.tr("Title Page")
# Settings
project.data.setUuid(None)
- project.data.setName(data.get("name", lblNewProject))
- project.data.setAuthor(data.get("author", ""))
+ project.data.setName(data.get("name", lblName))
+ project.data.setAuthor(data.get("author", lblAuthor))
project.data.setLanguage(CONFIG.guiLocale)
project.setDefaultStatusImport()
project.session.startSession()
@@ -446,18 +447,14 @@ class ProjectBuilder:
"\n"
">> {count}: [field:{field}] <<\n"
).format(
- author=project.data.author or "None",
- address=self.tr("Address"),
- title=project.data.name or "None",
+ author=project.data.author or lblAuthor,
+ address=self.tr("Address Line"),
+ title=project.data.name or lblName,
by=self.tr("By"),
count=self.tr("Word Count"),
field=nwStats.WORDS_TEXT,
))
- # Create a project structure based on selected root folders
- # and a number of chapters and scenes selected in the
- # wizard's custom page.
-
# Create chapters and scenes
numChapters = data.get("chapters", 0)
numScenes = data.get("scenes", 0)
@@ -563,19 +560,7 @@ class ProjectBuilder:
return False
# Open the copied project and update settings
- project = NWProject()
- project.openProject(dstPath)
- project.data.setUuid("") # Creates a fresh uuid
- project.data.setName(data.get("name", "None"))
- project.data.setAuthor(data.get("author", ""))
- project.data.setSpellCheck(True)
- project.data.setSpellLang(None)
- project.data.setDoBackup(True)
- project.data.setSaveCount(0)
- project.data.setAutoCount(0)
- project.data.setEditTime(0)
- project.saveProject()
- project.closeProject()
+ self._resetProject(dstPath, data.get("name", ""), data.get("author", ""))
return True
@@ -593,6 +578,7 @@ class ProjectBuilder:
if (sample := CONFIG.assetPath("sample.zip")).is_file():
try:
shutil.unpack_archive(sample, path)
+ self._resetProject(path)
except Exception as exc:
SHARED.error(self.tr("Failed to create a new example project."), exc=exc)
return False
@@ -605,3 +591,22 @@ class ProjectBuilder:
return False
return True
+
+ def _resetProject(self, path: Path, name: str = "", author: str = "") -> None:
+ """Open a project and reset/update its settings."""
+ project = NWProject()
+ project.openProject(path)
+ project.data.setUuid("") # Creates a fresh uuid
+ if name:
+ project.data.setName(name)
+ if author:
+ project.data.setAuthor(author)
+ project.data.setSpellCheck(True)
+ project.data.setSpellLang(None)
+ project.data.setDoBackup(True)
+ project.data.setSaveCount(0)
+ project.data.setAutoCount(0)
+ project.data.setEditTime(0)
+ project.saveProject()
+ project.closeProject()
+ return
diff --git a/tests/reference/coreTools_ProjectBuilderA_Project.md b/tests/reference/coreTools_ProjectBuilderA_Project.md
index d608ae09..359ed670 100644
--- a/tests/reference/coreTools_ProjectBuilderA_Project.md
+++ b/tests/reference/coreTools_ProjectBuilderA_Project.md
@@ -1,6 +1,6 @@
Jane Doe
-Address 1
-Address 2
+Address Line 1
+Address Line 2
@@ -17,7 +17,7 @@ Test Project A
**By Jane Doe**
-Word Count: 11
+Word Count: 13
# Chapter 1
diff --git a/tests/reference/coreTools_ProjectBuilderB_Project.md b/tests/reference/coreTools_ProjectBuilderB_Project.md
index 7f6329f2..e41c9b5d 100644
--- a/tests/reference/coreTools_ProjectBuilderB_Project.md
+++ b/tests/reference/coreTools_ProjectBuilderB_Project.md
@@ -1,6 +1,6 @@
Jane Doe
-Address 1
-Address 2
+Address Line 1
+Address Line 2
@@ -17,7 +17,7 @@ Test Project B
**By Jane Doe**
-Word Count: 11
+Word Count: 13
* * *
From c90929750441c21ef598d3f53ad3f299ce97d3da Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 11 May 2025 13:32:32 +0200
Subject: [PATCH 08/16] Remember last used author name
---
novelwriter/config.py | 35 ++++++++++++++-------
novelwriter/core/coretools.py | 32 ++++++++++---------
novelwriter/tools/welcome.py | 1 +
tests/reference/baseConfig_novelwriter.conf | 3 +-
tests/test_base/test_base_config.py | 5 +++
5 files changed, 49 insertions(+), 27 deletions(-)
diff --git a/novelwriter/config.py b/novelwriter/config.py
index d820c66a..1d2e47d8 100644
--- a/novelwriter/config.py
+++ b/novelwriter/config.py
@@ -42,7 +42,7 @@ from PyQt6.QtWidgets import QApplication
from novelwriter.common import (
NWConfigParser, checkInt, checkPath, describeFont, fontMatcher,
- formatTimeStamp, processDialogSymbols
+ formatTimeStamp, processDialogSymbols, simplified
)
from novelwriter.constants import nwFiles, nwHtmlUnicode, nwQuotes, nwUnicode
from novelwriter.error import formatException, logException
@@ -65,16 +65,16 @@ class Config:
__slots__ = (
"_appPath", "_appRoot", "_backPath", "_backupPath", "_confPath", "_dLocale", "_dShortDate",
- "_dShortDateTime", "_dataPath", "_errData", "_hasError", "_homePath", "_manuals",
- "_nwLangPath", "_qLocale", "_qtLangPath", "_qtTrans", "_recentPaths", "_recentProjects",
- "_splash", "allowOpenDial", "altDialogClose", "altDialogOpen", "appHandle", "appName",
- "askBeforeBackup", "askBeforeExit", "autoSaveDoc", "autoSaveProj", "autoScroll",
- "autoScrollPos", "autoSelect", "backupOnClose", "cursorWidth", "dialogLine", "dialogStyle",
- "doJustify", "doReplace", "doReplaceDQuote", "doReplaceDash", "doReplaceDots",
- "doReplaceSQuote", "emphLabels", "fmtApostrophe", "fmtDQuoteClose", "fmtDQuoteOpen",
- "fmtPadAfter", "fmtPadBefore", "fmtPadThin", "fmtSQuoteClose", "fmtSQuoteOpen",
- "focusWidth", "guiFont", "guiLocale", "guiSyntax", "guiTheme", "hasEnchant",
- "hideFocusFooter", "hideHScroll", "hideVScroll", "highlightEmph", "hostName",
+ "_dShortDateTime", "_dataPath", "_errData", "_hasError", "_homePath", "_lastAuthor",
+ "_manuals", "_nwLangPath", "_qLocale", "_qtLangPath", "_qtTrans", "_recentPaths",
+ "_recentProjects", "_splash", "allowOpenDial", "altDialogClose", "altDialogOpen",
+ "appHandle", "appName", "askBeforeBackup", "askBeforeExit", "autoSaveDoc", "autoSaveProj",
+ "autoScroll", "autoScrollPos", "autoSelect", "backupOnClose", "cursorWidth", "dialogLine",
+ "dialogStyle", "doJustify", "doReplace", "doReplaceDQuote", "doReplaceDash",
+ "doReplaceDots", "doReplaceSQuote", "emphLabels", "fmtApostrophe", "fmtDQuoteClose",
+ "fmtDQuoteOpen", "fmtPadAfter", "fmtPadBefore", "fmtPadThin", "fmtSQuoteClose",
+ "fmtSQuoteOpen", "focusWidth", "guiFont", "guiLocale", "guiSyntax", "guiTheme",
+ "hasEnchant", "hideFocusFooter", "hideHScroll", "hideVScroll", "highlightEmph", "hostName",
"iconColDocs", "iconColTree", "iconTheme", "incNotesWCount", "isDebug", "kernelVer",
"lastNotes", "mainPanePos", "mainWinSize", "memInfo", "narratorBreak", "narratorDialog",
"nativeFont", "osDarwin", "osLinux", "osType", "osUnknown", "osWindows", "outlinePanePos",
@@ -149,6 +149,7 @@ class Config:
self._recentProjects = RecentProjects(self)
self._recentPaths = RecentPaths(self)
+ self._lastAuthor = ""
# General GUI Settings
self.guiLocale = self._qLocale.name()
@@ -321,6 +322,11 @@ class Config:
def recentProjects(self) -> RecentProjects:
return self._recentProjects
+ @property
+ def lastAuthor(self) -> str:
+ """Return the last author name used."""
+ return simplified(self._lastAuthor)
+
##
# Getters
##
@@ -336,6 +342,11 @@ class Config:
# Setters
##
+ def setLastAuthor(self, value: str) -> None:
+ """Set tle last used author name."""
+ self._lastAuthor = simplified(value)
+ return
+
def setMainWinSize(self, width: int, height: int) -> None:
"""Set the size of the main window, but only if the change is
larger than 5 pixels. The OS window manager will sometimes
@@ -645,6 +656,7 @@ class Config:
self.backupOnClose = conf.rdBool(sec, "backuponclose", self.backupOnClose)
self.askBeforeBackup = conf.rdBool(sec, "askbeforebackup", self.askBeforeBackup)
self.askBeforeExit = conf.rdBool(sec, "askbeforeexit", self.askBeforeExit)
+ self._lastAuthor = conf.rdStr(sec, "lastauthor", self._lastAuthor)
# Editor
sec = "Editor"
@@ -769,6 +781,7 @@ class Config:
"backuponclose": str(self.backupOnClose),
"askbeforebackup": str(self.askBeforeBackup),
"askbeforeexit": str(self.askBeforeExit),
+ "lastauthor": str(self._lastAuthor),
}
conf["Editor"] = {
diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py
index 8c7b7195..4c8d6004 100644
--- a/novelwriter/core/coretools.py
+++ b/novelwriter/core/coretools.py
@@ -383,6 +383,8 @@ class ProjectBuilder:
"""Build or copy a project from a data dictionary."""
if isinstance(data, dict):
path = data.get("path", None) or None
+ if author := data.get("author"):
+ CONFIG.setLastAuthor(author)
if isinstance(path, str | Path):
self._path = Path(path).resolve()
if data.get("sample"):
@@ -416,21 +418,21 @@ class ProjectBuilder:
self._path = project.storage.storagePath
- lblName = self.tr("New Project")
- lblAuthor = self.tr("Author")
- lblTitlePage = self.tr("Title Page")
+ trName = self.tr("New Project")
+ trAuthor = self.tr("Author Name")
+ trTitlePage = self.tr("Title Page")
# Settings
project.data.setUuid(None)
- project.data.setName(data.get("name", lblName))
- project.data.setAuthor(data.get("author", lblAuthor))
+ project.data.setName(data.get("name", trName))
+ project.data.setAuthor(data.get("author", trAuthor))
project.data.setLanguage(CONFIG.guiLocale)
project.setDefaultStatusImport()
project.session.startSession()
# Add Root Folders
hNovelRoot = project.newRoot(nwItemClass.NOVEL)
- hTitlePage = project.newFile(lblTitlePage, hNovelRoot)
+ hTitlePage = project.newFile(trTitlePage, hNovelRoot)
# Generate Title Page
aDoc = project.storage.getDocument(hTitlePage)
@@ -447,9 +449,9 @@ class ProjectBuilder:
"\n"
">> {count}: [field:{field}] <<\n"
).format(
- author=project.data.author or lblAuthor,
+ author=project.data.author or trAuthor,
address=self.tr("Address Line"),
- title=project.data.name or lblName,
+ title=project.data.name or trName,
by=self.tr("By"),
count=self.tr("Word Count"),
field=nwStats.WORDS_TEXT,
@@ -459,9 +461,9 @@ class ProjectBuilder:
numChapters = data.get("chapters", 0)
numScenes = data.get("scenes", 0)
- chSynop = self.tr("Summary of the chapter.")
- scSynop = self.tr("Summary of the scene.")
- bfNote = self.tr("A short description.")
+ trChSynop = self.tr("Summary of the chapter.")
+ trScSynop = self.tr("Summary of the scene.")
+ trNoteDesc = self.tr("A short description.")
# Create chapters
if numChapters > 0:
@@ -469,7 +471,7 @@ class ProjectBuilder:
chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
cHandle = project.newFile(chTitle, hNovelRoot)
aDoc = project.storage.getDocument(cHandle)
- aDoc.writeDocument(f"## {chTitle}\n\n%Synopsis: {chSynop}\n\n")
+ aDoc.writeDocument(f"## {chTitle}\n\n%Synopsis: {trChSynop}\n\n")
# Create chapter scenes
if numScenes > 0 and cHandle:
@@ -477,7 +479,7 @@ class ProjectBuilder:
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
sHandle = project.newFile(scTitle, cHandle)
aDoc = project.storage.getDocument(sHandle)
- aDoc.writeDocument(f"### {scTitle}\n\n%Synopsis: {scSynop}\n\n")
+ aDoc.writeDocument(f"### {scTitle}\n\n%Synopsis: {trScSynop}\n\n")
# Create scenes (no chapters)
elif numScenes > 0:
@@ -485,7 +487,7 @@ class ProjectBuilder:
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
sHandle = project.newFile(scTitle, hNovelRoot)
aDoc = project.storage.getDocument(sHandle)
- aDoc.writeDocument(f"### {scTitle}\n\n%Synopsis: {scSynop}\n\n")
+ aDoc.writeDocument(f"### {scTitle}\n\n%Synopsis: {trScSynop}\n\n")
# Create notes folders
noteTitles = {
@@ -505,7 +507,7 @@ class ProjectBuilder:
aDoc.writeDocument(
f"# {noteTitles[newRoot]}\n\n"
f"@tag: {ntTag}\n\n"
- f"%Short: {bfNote}\n\n"
+ f"%Short: {trNoteDesc}\n\n"
)
# Also add the archive and trash folders
diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py
index 3638c18d..9b0abe33 100644
--- a/novelwriter/tools/welcome.py
+++ b/novelwriter/tools/welcome.py
@@ -565,6 +565,7 @@ class _NewProjectForm(QWidget):
self.projAuthor = QLineEdit(self)
self.projAuthor.setMaxLength(200)
self.projAuthor.setPlaceholderText(self.tr("Optional"))
+ self.projAuthor.setText(CONFIG.lastAuthor)
# Project Path
self.projPath = QLineEdit(self)
diff --git a/tests/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf
index fb701405..78441636 100644
--- a/tests/reference/baseConfig_novelwriter.conf
+++ b/tests/reference/baseConfig_novelwriter.conf
@@ -1,5 +1,5 @@
[Meta]
-timestamp = 2025-02-22 19:57:47
+timestamp = 2025-05-11 13:28:11
[Main]
font =
@@ -31,6 +31,7 @@ backuppath =
backuponclose = False
askbeforebackup = True
askbeforeexit = True
+lastauthor =
[Editor]
textfont =
diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py
index ee43b86a..81b03831 100644
--- a/tests/test_base/test_base_config.py
+++ b/tests/test_base/test_base_config.py
@@ -242,6 +242,11 @@ def testBaseConfig_Methods(fncPath):
# Recent Projects
assert isinstance(tstConf.recentProjects, RecentProjects)
+ # Last Author
+ assert CONFIG.lastAuthor == ""
+ CONFIG.setLastAuthor(" Jane Doe ")
+ assert CONFIG.lastAuthor == "Jane Doe"
+
@pytest.mark.base
def testBaseConfig_SettersGetters(fncPath):
From 08dda5d58b5de45c62a824c8a3257425d57ced7d Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 11 May 2025 13:42:05 +0200
Subject: [PATCH 09/16] Apply title and author also to sample projects
---
novelwriter/core/coretools.py | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py
index 4c8d6004..a4b2a0b1 100644
--- a/novelwriter/core/coretools.py
+++ b/novelwriter/core/coretools.py
@@ -388,7 +388,7 @@ class ProjectBuilder:
if isinstance(path, str | Path):
self._path = Path(path).resolve()
if data.get("sample"):
- return self._extractSampleProject(self._path)
+ return self._extractSampleProject(self._path, data)
elif data.get("template"):
return self._copyProject(self._path, data)
else:
@@ -562,11 +562,11 @@ class ProjectBuilder:
return False
# Open the copied project and update settings
- self._resetProject(dstPath, data.get("name", ""), data.get("author", ""))
+ self._resetProject(dstPath, data)
return True
- def _extractSampleProject(self, path: Path) -> bool:
+ def _extractSampleProject(self, path: Path, data: dict) -> bool:
"""Make a copy of the sample project by extracting the
sample.zip file to the new path.
"""
@@ -580,7 +580,7 @@ class ProjectBuilder:
if (sample := CONFIG.assetPath("sample.zip")).is_file():
try:
shutil.unpack_archive(sample, path)
- self._resetProject(path)
+ self._resetProject(path, data)
except Exception as exc:
SHARED.error(self.tr("Failed to create a new example project."), exc=exc)
return False
@@ -594,14 +594,14 @@ class ProjectBuilder:
return True
- def _resetProject(self, path: Path, name: str = "", author: str = "") -> None:
+ def _resetProject(self, path: Path, data: dict) -> None:
"""Open a project and reset/update its settings."""
project = NWProject()
project.openProject(path)
project.data.setUuid("") # Creates a fresh uuid
- if name:
+ if name := data.get("name", ""):
project.data.setName(name)
- if author:
+ if author := data.get("author", ""):
project.data.setAuthor(author)
project.data.setSpellCheck(True)
project.data.setSpellLang(None)
From 6fdc6cfaa3441d0448e59e1db0a9367376baa67b Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 11 May 2025 15:28:49 +0200
Subject: [PATCH 10/16] Add a Snazzy Light GUI theme
---
novelwriter/assets/themes/snazzy.conf | 48 +++++++++++++++++++++++++++
1 file changed, 48 insertions(+)
create mode 100644 novelwriter/assets/themes/snazzy.conf
diff --git a/novelwriter/assets/themes/snazzy.conf b/novelwriter/assets/themes/snazzy.conf
new file mode 100644
index 00000000..39ec3c87
--- /dev/null
+++ b/novelwriter/assets/themes/snazzy.conf
@@ -0,0 +1,48 @@
+[Main]
+name = Snazzy Light
+author = Veronica Berglyd Olsen (adaptation)
+credit = Florian Reuschel (color theme)
+url = https://github.com/loilo/vscode-snazzy-light
+license = MIT License
+licenseurl = https://github.com/loilo/vscode-snazzy-light/blob/master/LICENSE
+
+[Icons]
+default = 86, 88, 105
+faded = 84, 85, 84
+red = 255, 92, 87
+orange = 245, 185, 0
+yellow = 207, 156, 0
+green = 45, 174, 88
+aqua = 19, 187, 183
+blue = 9, 161, 237
+purple = 247, 103, 187
+
+[Project]
+root = 9, 161, 237
+folder = 207, 156, 0
+file = 84, 85, 84
+title = 45, 174, 88
+chapter = 255, 92, 87
+scene = 9, 161, 237
+note = 207, 156, 0
+
+[Palette]
+window = 243, 244, 245
+windowtext = 86, 88, 105
+base = 250, 251, 252
+alternatebase = 234, 234, 235
+text = 86, 88, 105
+tooltipbase = 245, 233, 194
+tooltiptext = 86, 88, 105
+button = 250, 251, 252
+buttontext = 86, 88, 105
+brighttext = 255, 255, 255
+highlight = 9, 161, 237
+highlightedtext = 255, 255, 255
+link = 9, 161, 237
+linkvisited = 9, 161, 237
+
+[GUI]
+helptext = 9, 161, 237
+fadedtext = 84, 85, 84
+errortext = 255, 92, 87
From c0d9f00adb512c318308721345a0f8963e805e27 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 11 May 2025 16:48:48 +0200
Subject: [PATCH 11/16] Improve the Snazzy Light syntax theme a little
---
novelwriter/assets/syntax/snazzy.conf | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/novelwriter/assets/syntax/snazzy.conf b/novelwriter/assets/syntax/snazzy.conf
index da979a00..d948b770 100644
--- a/novelwriter/assets/syntax/snazzy.conf
+++ b/novelwriter/assets/syntax/snazzy.conf
@@ -25,15 +25,15 @@ text = 86, 88, 105
link = 9, 161, 237
headertext = 45, 174, 88
headertag = 45, 174, 88, 160
-emphasis = 19, 187, 183
-dialog = 207, 156, 0
+emphasis = 247, 103, 187
+dialog = 9, 161, 237
altdialog = 207, 156, 0
note = 120, 187, 185
hidden = 145, 148, 162
shortcode = 247, 103, 187
keyword = 9, 161, 237
tag = 45, 174, 88
-value = 9, 161, 237
+value = 207, 156, 0
optional = 207, 156, 0
spellcheckline = 255, 92, 87
errorline = 45, 174, 88
From e200e6b9007758d6423941b8a16cc380563e979d Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 11 May 2025 17:33:04 +0200
Subject: [PATCH 12/16] Change how the switch is animated
---
novelwriter/extensions/switch.py | 29 +++++++++++++++--------------
1 file changed, 15 insertions(+), 14 deletions(-)
diff --git a/novelwriter/extensions/switch.py b/novelwriter/extensions/switch.py
index 421fb712..b915622e 100644
--- a/novelwriter/extensions/switch.py
+++ b/novelwriter/extensions/switch.py
@@ -23,12 +23,12 @@ along with this program. If not, see .
"""
from __future__ import annotations
-from PyQt6.QtCore import QPropertyAnimation, Qt, pyqtProperty # pyright: ignore
-from PyQt6.QtGui import QEnterEvent, QMouseEvent, QPainter, QPaintEvent, QResizeEvent
+from PyQt6.QtCore import QPropertyAnimation, Qt, pyqtProperty, pyqtSlot # pyright: ignore
+from PyQt6.QtGui import QEnterEvent, QPainter, QPaintEvent, QResizeEvent
from PyQt6.QtWidgets import QAbstractButton, QWidget
from novelwriter import SHARED
-from novelwriter.types import QtMouseLeft, QtNoPen, QtPaintAntiAlias, QtSizeFixed
+from novelwriter.types import QtNoPen, QtPaintAntiAlias, QtSizeFixed
class NSwitch(QAbstractButton):
@@ -50,6 +50,8 @@ class NSwitch(QAbstractButton):
self.setFixedHeight(self._xH)
self._offset = self._xR
+ self.clicked.connect(self._onClick)
+
return
##
@@ -106,19 +108,18 @@ class NSwitch(QAbstractButton):
return
- def mouseReleaseEvent(self, event: QMouseEvent) -> None:
- """Animate the switch on mouse release."""
- super().mouseReleaseEvent(event)
- if event.button() == QtMouseLeft:
- anim = QPropertyAnimation(self, b"offset", self)
- anim.setDuration(120)
- anim.setStartValue(self._offset)
- anim.setEndValue((self._xW - self._xR) if self.isChecked() else self._xR)
- anim.start()
- return
-
def enterEvent(self, event: QEnterEvent) -> None:
"""Change the cursor when hovering the button."""
self.setCursor(Qt.CursorShape.PointingHandCursor)
super().enterEvent(event)
return
+
+ @pyqtSlot(bool)
+ def _onClick(self, checked: bool) -> None:
+ """Animate the toggle action."""
+ anim = QPropertyAnimation(self, b"offset", self)
+ anim.setDuration(120)
+ anim.setStartValue(self._offset)
+ anim.setEndValue((self._xW - self._xR) if checked else self._xR)
+ anim.start()
+ return
From 4e98914a8b72e55770a58f2e105252227846bc19 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 11 May 2025 18:08:33 +0200
Subject: [PATCH 13/16] Link switches and their labels
---
novelwriter/dialogs/docmerge.py | 3 +-
novelwriter/dialogs/docsplit.py | 9 +-
novelwriter/dialogs/editlabel.py | 19 ++--
novelwriter/extensions/switchbox.py | 1 +
novelwriter/tools/lipsum.py | 3 +-
novelwriter/tools/manussettings.py | 46 +++++---
novelwriter/tools/noveldetails.py | 12 +--
novelwriter/tools/writingstats.py | 111 ++++++++++++--------
tests/test_tools/test_tools_writingstats.py | 18 ++--
9 files changed, 137 insertions(+), 85 deletions(-)
diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py
index 97717dc6..9345efd6 100644
--- a/novelwriter/dialogs/docmerge.py
+++ b/novelwriter/dialogs/docmerge.py
@@ -73,8 +73,9 @@ class GuiDocMerge(NDialog):
self.listBox.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
# Merge Options
- self.trashLabel = QLabel(self.tr("Move merged items to Trash"), self)
self.trashSwitch = NSwitch(self, height=iPx)
+ self.trashLabel = QLabel(self.tr("Move merged items to Trash"), self)
+ self.trashLabel.setBuddy(self.trashSwitch)
self.optBox = QGridLayout()
self.optBox.addWidget(self.trashLabel, 0, 0)
diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py
index 3f1f34ad..a67aafa7 100644
--- a/novelwriter/dialogs/docsplit.py
+++ b/novelwriter/dialogs/docsplit.py
@@ -90,16 +90,19 @@ class GuiDocSplit(NDialog):
self.splitLevel.currentIndexChanged.connect(self._reloadList)
# Split Options
- self.folderLabel = QLabel(self.tr("Split into a new folder"), self)
self.folderSwitch = NSwitch(self, height=iPx)
self.folderSwitch.setChecked(intoFolder)
+ self.folderLabel = QLabel(self.tr("Split into a new folder"), self)
+ self.folderLabel.setBuddy(self.folderSwitch)
- self.hierarchyLabel = QLabel(self.tr("Create document hierarchy"), self)
self.hierarchySwitch = NSwitch(self, height=iPx)
self.hierarchySwitch.setChecked(docHierarchy)
+ self.hierarchyLabel = QLabel(self.tr("Create document hierarchy"), self)
+ self.hierarchyLabel.setBuddy(self.hierarchySwitch)
- self.trashLabel = QLabel(self.tr("Move split document to Trash"), self)
self.trashSwitch = NSwitch(self, height=iPx)
+ self.trashLabel = QLabel(self.tr("Move split document to Trash"), self)
+ self.trashLabel.setBuddy(self.trashSwitch)
self.optBox = QGridLayout()
self.optBox.addWidget(self.folderLabel, 0, 0)
diff --git a/novelwriter/dialogs/editlabel.py b/novelwriter/dialogs/editlabel.py
index 5386acb4..97bb269c 100644
--- a/novelwriter/dialogs/editlabel.py
+++ b/novelwriter/dialogs/editlabel.py
@@ -43,11 +43,14 @@ class GuiEditLabel(NDialog):
self.setWindowTitle(self.tr("Item Label"))
# Item Label
- self.labelValue = QLineEdit(self)
- self.labelValue.setMinimumWidth(220)
- self.labelValue.setMaxLength(200)
- self.labelValue.setText(text)
- self.labelValue.selectAll()
+ self.edtValue = QLineEdit(self)
+ self.edtValue.setMinimumWidth(220)
+ self.edtValue.setMaxLength(200)
+ self.edtValue.setText(text)
+ self.edtValue.selectAll()
+
+ self.lblValue = QLabel(self.tr("Label"), self)
+ self.lblValue.setBuddy(self.lblValue)
# Buttons
self.buttonBox = QDialogButtonBox(QtDialogOk | QtDialogCancel, self)
@@ -56,8 +59,8 @@ class GuiEditLabel(NDialog):
# Assemble
self.innerBox = QHBoxLayout()
- self.innerBox.addWidget(QLabel(self.tr("Label"), self), 0)
- self.innerBox.addWidget(self.labelValue, 1)
+ self.innerBox.addWidget(self.lblValue, 0)
+ self.innerBox.addWidget(self.edtValue, 1)
self.innerBox.setSpacing(12)
self.outerBox = QVBoxLayout()
@@ -77,7 +80,7 @@ class GuiEditLabel(NDialog):
@property
def itemLabel(self) -> str:
- return self.labelValue.text()
+ return self.edtValue.text()
@classmethod
def getLabel(cls, parent: QWidget, text: str) -> tuple[str, bool]:
diff --git a/novelwriter/extensions/switchbox.py b/novelwriter/extensions/switchbox.py
index c43def8a..e4732f97 100644
--- a/novelwriter/extensions/switchbox.py
+++ b/novelwriter/extensions/switchbox.py
@@ -100,6 +100,7 @@ class NSwitchBox(QScrollArea):
switch.toggled.connect(lambda state: self._emitSwitchSignal(identifier, state))
self._content.addWidget(switch, self._index, 2, QtAlignRight)
+ label.setBuddy(switch)
self._widgets.append(switch)
self._bumpIndex()
diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py
index b2028dca..4e54d79e 100644
--- a/novelwriter/tools/lipsum.py
+++ b/novelwriter/tools/lipsum.py
@@ -75,8 +75,9 @@ class GuiLipsum(NDialog):
self.paraCount.setMaximum(100)
self.paraCount.setValue(5)
- self.randLabel = QLabel(self.tr("Randomise order"), self)
self.randSwitch = NSwitch(self)
+ self.randLabel = QLabel(self.tr("Randomise order"), self)
+ self.randLabel.setBuddy(self.randSwitch)
self.formBox = QGridLayout()
self.formBox.addWidget(self.headLabel, 0, 0, 1, 2, QtAlignLeft)
diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py
index ee802487..3fa9bb03 100644
--- a/novelwriter/tools/manussettings.py
+++ b/novelwriter/tools/manussettings.py
@@ -564,9 +564,10 @@ class _HeadingsTab(NScrollablePage):
self.fmtPart.setReadOnly(True)
self.btnPart = NIconToolButton(self, iSz, "edit", "green")
self.btnPart.clicked.connect(qtLambda(self._editHeading, self.EDIT_TITLE))
+ self.swtPart = NSwitch(self, height=iPx)
self.hdePart = QLabel(trHide, self)
self.hdePart.setIndent(6)
- self.swtPart = NSwitch(self, height=iPx)
+ self.hdePart.setBuddy(self.swtPart)
self.formatBox.addWidget(self.lblPart, 0, 0)
self.formatBox.addWidget(self.fmtPart, 0, 1)
@@ -580,9 +581,10 @@ class _HeadingsTab(NScrollablePage):
self.fmtChapter.setReadOnly(True)
self.btnChapter = NIconToolButton(self, iSz, "edit", "green")
self.btnChapter.clicked.connect(qtLambda(self._editHeading, self.EDIT_CHAPTER))
+ self.swtChapter = NSwitch(self, height=iPx)
self.hdeChapter = QLabel(trHide, self)
self.hdeChapter.setIndent(6)
- self.swtChapter = NSwitch(self, height=iPx)
+ self.hdeChapter.setBuddy(self.swtChapter)
self.formatBox.addWidget(self.lblChapter, 1, 0)
self.formatBox.addWidget(self.fmtChapter, 1, 1)
@@ -596,9 +598,10 @@ class _HeadingsTab(NScrollablePage):
self.fmtUnnumbered.setReadOnly(True)
self.btnUnnumbered = NIconToolButton(self, iSz, "edit", "green")
self.btnUnnumbered.clicked.connect(qtLambda(self._editHeading, self.EDIT_UNNUM))
+ self.swtUnnumbered = NSwitch(self, height=iPx)
self.hdeUnnumbered = QLabel(trHide, self)
self.hdeUnnumbered.setIndent(6)
- self.swtUnnumbered = NSwitch(self, height=iPx)
+ self.hdeUnnumbered.setBuddy(self.swtUnnumbered)
self.formatBox.addWidget(self.lblUnnumbered, 2, 0)
self.formatBox.addWidget(self.fmtUnnumbered, 2, 1)
@@ -612,9 +615,10 @@ class _HeadingsTab(NScrollablePage):
self.fmtScene.setReadOnly(True)
self.btnScene = NIconToolButton(self, iSz, "edit", "green")
self.btnScene.clicked.connect(qtLambda(self._editHeading, self.EDIT_SCENE))
+ self.swtScene = NSwitch(self, height=iPx)
self.hdeScene = QLabel(trHide, self)
self.hdeScene.setIndent(6)
- self.swtScene = NSwitch(self, height=iPx)
+ self.hdeScene.setBuddy(self.swtScene)
self.formatBox.addWidget(self.lblScene, 3, 0)
self.formatBox.addWidget(self.fmtScene, 3, 1)
@@ -628,9 +632,10 @@ class _HeadingsTab(NScrollablePage):
self.fmtAScene.setReadOnly(True)
self.btnAScene = NIconToolButton(self, iSz, "edit", "green")
self.btnAScene.clicked.connect(qtLambda(self._editHeading, self.EDIT_HSCENE))
+ self.swtAScene = NSwitch(self, height=iPx)
self.hdeAScene = QLabel(trHide, self)
self.hdeAScene.setIndent(6)
- self.swtAScene = NSwitch(self, height=iPx)
+ self.hdeAScene.setBuddy(self.swtAScene)
self.formatBox.addWidget(self.lblAScene, 4, 0)
self.formatBox.addWidget(self.fmtAScene, 4, 1)
@@ -644,9 +649,10 @@ class _HeadingsTab(NScrollablePage):
self.fmtSection.setReadOnly(True)
self.btnSection = NIconToolButton(self, iSz, "edit", "green")
self.btnSection.clicked.connect(qtLambda(self._editHeading, self.EDIT_SECTION))
+ self.swtSection = NSwitch(self, height=iPx)
self.hdeSection = QLabel(trHide, self)
self.hdeSection.setIndent(6)
- self.swtSection = NSwitch(self, height=iPx)
+ self.hdeSection.setBuddy(self.swtSection)
self.formatBox.addWidget(self.lblSection, 5, 0)
self.formatBox.addWidget(self.fmtSection, 5, 1)
@@ -704,44 +710,59 @@ class _HeadingsTab(NScrollablePage):
# Layout Matrix
# =============
+ trCentre = self.tr("Centre")
+ trBreak = self.tr("Page Break")
+
self.layoutMatrix = QGridLayout()
self.layoutMatrix.setVerticalSpacing(12)
self.layoutMatrix.setHorizontalSpacing(12)
- self.layoutMatrix.addWidget(QLabel(self.tr("Centre"), self), 0, 1)
- self.layoutMatrix.addWidget(QLabel(self.tr("Page Break"), self), 0, 2)
+ self.layoutMatrix.addWidget(QLabel(trCentre, self), 0, 1)
+ self.layoutMatrix.addWidget(QLabel(trBreak, self), 0, 2)
# Title Layout
- self.lblTitle = QLabel(self._build.getLabel("headings.styleTitle"), self)
+ trLabel = self._build.getLabel("headings.styleTitle")
+ self.lblTitle = QLabel(trLabel, self)
self.centerTitle = NSwitch(self, height=iPx)
+ self.centerTitle.setAccessibleName(f"{trLabel}: {trCentre}")
self.breakTitle = NSwitch(self, height=iPx)
+ self.breakTitle.setAccessibleName(f"{trLabel}: {trBreak}")
self.layoutMatrix.addWidget(self.lblTitle, 1, 0)
self.layoutMatrix.addWidget(self.centerTitle, 1, 1, QtAlignCenter)
self.layoutMatrix.addWidget(self.breakTitle, 1, 2, QtAlignCenter)
# Partition Layout
- self.lblPart = QLabel(self._build.getLabel("headings.stylePart"), self)
+ trLabel = self._build.getLabel("headings.stylePart")
+ self.lblPart = QLabel(trLabel, self)
self.centerPart = NSwitch(self, height=iPx)
+ self.centerPart.setAccessibleName(f"{trLabel}: {trCentre}")
self.breakPart = NSwitch(self, height=iPx)
+ self.breakPart.setAccessibleName(f"{trLabel}: {trBreak}")
self.layoutMatrix.addWidget(self.lblPart, 2, 0)
self.layoutMatrix.addWidget(self.centerPart, 2, 1, QtAlignCenter)
self.layoutMatrix.addWidget(self.breakPart, 2, 2, QtAlignCenter)
# Chapter Layout
- self.lblChapter = QLabel(self._build.getLabel("headings.styleChapter"), self)
+ trLabel = self._build.getLabel("headings.styleChapter")
+ self.lblChapter = QLabel(trLabel, self)
self.centerChapter = NSwitch(self, height=iPx)
+ self.centerChapter.setAccessibleName(f"{trLabel}: {trCentre}")
self.breakChapter = NSwitch(self, height=iPx)
+ self.breakChapter.setAccessibleName(f"{trLabel}: {trBreak}")
self.layoutMatrix.addWidget(self.lblChapter, 3, 0)
self.layoutMatrix.addWidget(self.centerChapter, 3, 1, QtAlignCenter)
self.layoutMatrix.addWidget(self.breakChapter, 3, 2, QtAlignCenter)
# Scene Layout
- self.lblScene = QLabel(self._build.getLabel("headings.styleScene"), self)
+ trLabel = self._build.getLabel("headings.styleScene")
+ self.lblScene = QLabel(trLabel, self)
self.centerScene = NSwitch(self, height=iPx)
+ self.centerScene.setAccessibleName(f"{trLabel}: {trCentre}")
self.breakScene = NSwitch(self, height=iPx)
+ self.breakScene.setAccessibleName(f"{trLabel}: {trBreak}")
self.layoutMatrix.addWidget(self.lblScene, 4, 0)
self.layoutMatrix.addWidget(self.centerScene, 4, 1, QtAlignCenter)
@@ -979,7 +1000,6 @@ class _FormattingTab(NScrollableForm):
# Note Headings
self.addNoteHead = NSwitch(self, height=iPx)
-
self.addRow(self._build.getLabel("text.addNoteHeadings"), self.addNoteHead)
# Text Format
diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py
index ec42ccf1..fbbcbb5d 100644
--- a/novelwriter/tools/noveldetails.py
+++ b/novelwriter/tools/noveldetails.py
@@ -356,16 +356,14 @@ class _ContentsPage(NFixedPage):
countFrom = options.getInt("GuiNovelDetails", "countFrom", 1)
clearDouble = options.getBool("GuiNovelDetails", "clearDouble", True)
- self.wpLabel = QLabel(self.tr("Words per page"), self)
-
self.wpValue = QSpinBox(self)
self.wpValue.setMinimum(10)
self.wpValue.setMaximum(1000)
self.wpValue.setSingleStep(10)
self.wpValue.setValue(wordsPerPage)
self.wpValue.valueChanged.connect(self._populateTree)
-
- self.poLabel = QLabel(self.tr("First page offset"), self)
+ self.wpLabel = QLabel(self.tr("Words per page"), self)
+ self.wpLabel.setBuddy(self.wpValue)
self.poValue = QSpinBox(self)
self.poValue.setMinimum(1)
@@ -373,12 +371,14 @@ class _ContentsPage(NFixedPage):
self.poValue.setSingleStep(1)
self.poValue.setValue(countFrom)
self.poValue.valueChanged.connect(self._populateTree)
-
- self.dblLabel = QLabel(self.tr("Chapters on odd pages"), self)
+ self.poLabel = QLabel(self.tr("First page offset"), self)
+ self.poLabel.setBuddy(self.poValue)
self.dblValue = NSwitch(self, height=iPx)
self.dblValue.setChecked(clearDouble)
self.dblValue.clicked.connect(self._populateTree)
+ self.dblLabel = QLabel(self.tr("Chapters on odd pages"), self)
+ self.dblLabel.setBuddy(self.dblValue)
self.optionsBox = QGridLayout()
self.optionsBox.addWidget(self.wpLabel, 0, 0)
diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py
index f2c14be2..0cbb9124 100644
--- a/novelwriter/tools/writingstats.py
+++ b/novelwriter/tools/writingstats.py
@@ -183,59 +183,82 @@ class GuiWritingStats(NToolDialog):
# Filter Options
iPx = SHARED.theme.baseIconHeight
- self.filterBox = QGroupBox(self.tr("Filters"), self)
self.filterForm = QGridLayout(self)
+ self.filterForm.setRowStretch(6, 1)
+ self.filterBox = QGroupBox(self.tr("Filters"), self)
self.filterBox.setLayout(self.filterForm)
- self.incNovel = NSwitch(self, height=iPx)
- self.incNovel.setChecked(
+ # Include Novel Files
+ self.swtIncNovel = NSwitch(self, height=iPx)
+ self.swtIncNovel.setChecked(
pOptions.getBool("GuiWritingStats", "incNovel", True)
)
- self.incNovel.clicked.connect(self._updateListBox)
+ self.swtIncNovel.clicked.connect(self._updateListBox)
+ self.lblIncNovel = QLabel(self.tr("Count novel files"), self)
+ self.lblIncNovel.setBuddy(self.swtIncNovel)
- self.incNotes = NSwitch(self, height=iPx)
- self.incNotes.setChecked(
+ self.filterForm.addWidget(self.lblIncNovel, 0, 0)
+ self.filterForm.addWidget(self.swtIncNovel, 0, 1)
+
+ # Include Note Files
+ self.swtIncNotes = NSwitch(self, height=iPx)
+ self.swtIncNotes.setChecked(
pOptions.getBool("GuiWritingStats", "incNotes", True)
)
- self.incNotes.clicked.connect(self._updateListBox)
+ self.swtIncNotes.clicked.connect(self._updateListBox)
+ self.lblIncNotes = QLabel(self.tr("Count note files"), self)
+ self.lblIncNotes.setBuddy(self.swtIncNotes)
- self.hideZeros = NSwitch(self, height=iPx)
- self.hideZeros.setChecked(
+ self.filterForm.addWidget(self.lblIncNotes, 1, 0)
+ self.filterForm.addWidget(self.swtIncNotes, 1, 1)
+
+ # Hide Zero Counts
+ self.swtHideZeros = NSwitch(self, height=iPx)
+ self.swtHideZeros.setChecked(
pOptions.getBool("GuiWritingStats", "hideZeros", True)
)
- self.hideZeros.clicked.connect(self._updateListBox)
+ self.swtHideZeros.clicked.connect(self._updateListBox)
+ self.lblHideZeros = QLabel(self.tr("Hide zero word count"), self)
+ self.lblHideZeros.setBuddy(self.swtHideZeros)
- self.hideNegative = NSwitch(self, height=iPx)
- self.hideNegative.setChecked(
+ self.filterForm.addWidget(self.lblHideZeros, 2, 0)
+ self.filterForm.addWidget(self.swtHideZeros, 2, 1)
+
+ # Hide Negative Counts
+ self.swtHideNegative = NSwitch(self, height=iPx)
+ self.swtHideNegative.setChecked(
pOptions.getBool("GuiWritingStats", "hideNegative", False)
)
- self.hideNegative.clicked.connect(self._updateListBox)
+ self.swtHideNegative.clicked.connect(self._updateListBox)
+ self.lblHideNegative = QLabel(self.tr("Hide negative word count"), self)
+ self.lblHideNegative.setBuddy(self.swtHideNegative)
- self.groupByDay = NSwitch(self, height=iPx)
- self.groupByDay.setChecked(
+ self.filterForm.addWidget(self.lblHideNegative, 3, 0)
+ self.filterForm.addWidget(self.swtHideNegative, 3, 1)
+
+ # Group Entries
+ self.swtGroupByDay = NSwitch(self, height=iPx)
+ self.swtGroupByDay.setChecked(
pOptions.getBool("GuiWritingStats", "groupByDay", False)
)
- self.groupByDay.clicked.connect(self._updateListBox)
+ self.swtGroupByDay.clicked.connect(self._updateListBox)
+ self.lblGroupByDay = QLabel(self.tr("Group entries by day"), self)
+ self.lblGroupByDay.setBuddy(self.swtGroupByDay)
- self.showIdleTime = NSwitch(self, height=iPx)
- self.showIdleTime.setChecked(
+ self.filterForm.addWidget(self.lblGroupByDay, 4, 0)
+ self.filterForm.addWidget(self.swtGroupByDay, 4, 1)
+
+ # Show Idle
+ self.swtShowIdleTime = NSwitch(self, height=iPx)
+ self.swtShowIdleTime.setChecked(
pOptions.getBool("GuiWritingStats", "showIdleTime", False)
)
- self.showIdleTime.clicked.connect(self._updateListBox)
+ self.swtShowIdleTime.clicked.connect(self._updateListBox)
+ self.lblShowIdleTime = QLabel(self.tr("Show idle time"), self)
+ self.lblShowIdleTime.setBuddy(self.swtShowIdleTime)
- self.filterForm.addWidget(QLabel(self.tr("Count novel files"), self), 0, 0)
- self.filterForm.addWidget(QLabel(self.tr("Count note files"), self), 1, 0)
- self.filterForm.addWidget(QLabel(self.tr("Hide zero word count"), self), 2, 0)
- self.filterForm.addWidget(QLabel(self.tr("Hide negative word count"), self), 3, 0)
- self.filterForm.addWidget(QLabel(self.tr("Group entries by day"), self), 4, 0)
- self.filterForm.addWidget(QLabel(self.tr("Show idle time"), self), 5, 0)
- self.filterForm.addWidget(self.incNovel, 0, 1)
- self.filterForm.addWidget(self.incNotes, 1, 1)
- self.filterForm.addWidget(self.hideZeros, 2, 1)
- self.filterForm.addWidget(self.hideNegative, 3, 1)
- self.filterForm.addWidget(self.groupByDay, 4, 1)
- self.filterForm.addWidget(self.showIdleTime, 5, 1)
- self.filterForm.setRowStretch(6, 1)
+ self.filterForm.addWidget(self.lblShowIdleTime, 5, 0)
+ self.filterForm.addWidget(self.swtShowIdleTime, 5, 1)
# Settings
self.histMax = QSpinBox(self)
@@ -325,12 +348,12 @@ class GuiWritingStats(NToolDialog):
sortCol = self.listBox.sortColumn()
sortOrder = header.sortIndicatorOrder() if header else 0
- incNovel = self.incNovel.isChecked()
- incNotes = self.incNotes.isChecked()
- hideZeros = self.hideZeros.isChecked()
- hideNegative = self.hideNegative.isChecked()
- groupByDay = self.groupByDay.isChecked()
- showIdleTime = self.showIdleTime.isChecked()
+ incNovel = self.swtIncNovel.isChecked()
+ incNotes = self.swtIncNotes.isChecked()
+ hideZeros = self.swtHideZeros.isChecked()
+ hideNegative = self.swtHideNegative.isChecked()
+ groupByDay = self.swtGroupByDay.isChecked()
+ showIdleTime = self.swtShowIdleTime.isChecked()
histMax = self.histMax.value()
logger.debug("Saving State: GuiWritingStats")
@@ -487,11 +510,11 @@ class GuiWritingStats(NToolDialog):
self.listBox.clear()
self.timeFilter = 0.0
- incNovel = self.incNovel.isChecked()
- incNotes = self.incNotes.isChecked()
- hideZeros = self.hideZeros.isChecked()
- hideNegative = self.hideNegative.isChecked()
- groupByDay = self.groupByDay.isChecked()
+ incNovel = self.swtIncNovel.isChecked()
+ incNotes = self.swtIncNotes.isChecked()
+ hideZeros = self.swtHideZeros.isChecked()
+ hideNegative = self.swtHideNegative.isChecked()
+ groupByDay = self.swtGroupByDay.isChecked()
histMax = self.histMax.value()
# Group the data
@@ -563,7 +586,7 @@ class GuiWritingStats(NToolDialog):
# Populate the list
mTrans = Qt.TransformationMode.FastTransformation
mAspect = Qt.AspectRatioMode.IgnoreAspectRatio
- showIdleTime = self.showIdleTime.isChecked()
+ showIdleTime = self.swtShowIdleTime.isChecked()
for _, sStart, sDiff, nWords, _, _, sIdle in self.filterData:
if showIdleTime:
diff --git a/tests/test_tools/test_tools_writingstats.py b/tests/test_tools/test_tools_writingstats.py
index 298b8540..0e2bc4ee 100644
--- a/tests/test_tools/test_tools_writingstats.py
+++ b/tests/test_tools/test_tools_writingstats.py
@@ -261,7 +261,7 @@ def testToolWritingStats_Filters(qtbot, monkeypatch, nwGUI, projPath, tstPaths):
assert item.text(sessLog.C_COUNT) == f"{200:n}"
# No Novel Files
- qtbot.mouseClick(sessLog.incNovel, QtMouseLeft)
+ qtbot.mouseClick(sessLog.swtIncNovel, QtMouseLeft)
assert sessLog._saveData(sessLog.FMT_JSON)
jsonStats = tstPaths.tmpDir / "sessionStats.json"
@@ -329,8 +329,8 @@ def testToolWritingStats_Filters(qtbot, monkeypatch, nwGUI, projPath, tstPaths):
]
# No Note Files
- qtbot.mouseClick(sessLog.incNovel, QtMouseLeft)
- qtbot.mouseClick(sessLog.incNotes, QtMouseLeft)
+ qtbot.mouseClick(sessLog.swtIncNovel, QtMouseLeft)
+ qtbot.mouseClick(sessLog.swtIncNotes, QtMouseLeft)
assert sessLog._saveData(sessLog.FMT_JSON)
jsonStats = tstPaths.tmpDir / "sessionStats.json"
@@ -398,8 +398,8 @@ def testToolWritingStats_Filters(qtbot, monkeypatch, nwGUI, projPath, tstPaths):
]
# No Negative Entries
- qtbot.mouseClick(sessLog.incNotes, QtMouseLeft)
- qtbot.mouseClick(sessLog.hideNegative, QtMouseLeft)
+ qtbot.mouseClick(sessLog.swtIncNotes, QtMouseLeft)
+ qtbot.mouseClick(sessLog.swtHideNegative, QtMouseLeft)
assert sessLog._saveData(sessLog.FMT_JSON)
jsonStats = tstPaths.tmpDir / "sessionStats.json"
@@ -439,8 +439,8 @@ def testToolWritingStats_Filters(qtbot, monkeypatch, nwGUI, projPath, tstPaths):
]
# Un-hide Zero Entries
- qtbot.mouseClick(sessLog.hideNegative, QtMouseLeft)
- qtbot.mouseClick(sessLog.hideZeros, QtMouseLeft)
+ qtbot.mouseClick(sessLog.swtHideNegative, QtMouseLeft)
+ qtbot.mouseClick(sessLog.swtHideZeros, QtMouseLeft)
assert sessLog._saveData(sessLog.FMT_JSON)
jsonStats = tstPaths.tmpDir / "sessionStats.json"
@@ -526,14 +526,14 @@ def testToolWritingStats_Filters(qtbot, monkeypatch, nwGUI, projPath, tstPaths):
assert item is not None
assert item.text(sessLog.C_IDLE) == "4 %"
- qtbot.mouseClick(sessLog.showIdleTime, QtMouseLeft)
+ qtbot.mouseClick(sessLog.swtShowIdleTime, QtMouseLeft)
item = sessLog.listBox.topLevelItem(7)
assert item is not None
assert item.text(sessLog.C_IDLE) == "00:01:10"
# Group by Day
- qtbot.mouseClick(sessLog.groupByDay, QtMouseLeft)
+ qtbot.mouseClick(sessLog.swtGroupByDay, QtMouseLeft)
assert sessLog._saveData(sessLog.FMT_JSON)
jsonStats = tstPaths.tmpDir / "sessionStats.json"
From 4417e275fdb7d4fe22e454ef7573f08786f5fb3b Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 11 May 2025 18:12:58 +0200
Subject: [PATCH 14/16] Add highlight to switch when it has focus
---
novelwriter/extensions/switch.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/novelwriter/extensions/switch.py b/novelwriter/extensions/switch.py
index b915622e..4699d50a 100644
--- a/novelwriter/extensions/switch.py
+++ b/novelwriter/extensions/switch.py
@@ -96,7 +96,7 @@ class NSwitch(QAbstractButton):
painter.setRenderHint(QtPaintAntiAlias, True)
painter.setOpacity(1.0 if self.isEnabled() else 0.5)
- painter.setPen(palette.mid().color())
+ painter.setPen(palette.highlight().color() if self.hasFocus() else palette.mid().color())
painter.setBrush(palette.highlight() if self.isChecked() else palette.alternateBase())
painter.drawRoundedRect(0, 0, self._xW, self._xH, self._xR, self._xR)
From 9b0890c9b72243aa5ef35b374bca53469a716504 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 11 May 2025 18:32:10 +0200
Subject: [PATCH 15/16] Improve accessibility labels for novel and project
trees
---
novelwriter/core/itemmodel.py | 28 ++++++++++++++++------------
novelwriter/core/novelmodel.py | 2 ++
novelwriter/gui/noveltree.py | 1 +
novelwriter/gui/projtree.py | 1 +
novelwriter/gui/search.py | 1 +
5 files changed, 21 insertions(+), 12 deletions(-)
diff --git a/novelwriter/core/itemmodel.py b/novelwriter/core/itemmodel.py
index 6fb2580a..ac7cb738 100644
--- a/novelwriter/core/itemmodel.py
+++ b/novelwriter/core/itemmodel.py
@@ -45,16 +45,18 @@ logger = logging.getLogger(__name__)
INV_ROOT = "invisibleRoot"
C_FACTOR = 0x0100
-C_LABEL_TEXT = 0x0000 | Qt.ItemDataRole.DisplayRole
-C_LABEL_ICON = 0x0000 | Qt.ItemDataRole.DecorationRole
-C_LABEL_FONT = 0x0000 | Qt.ItemDataRole.FontRole
-C_COUNT_TEXT = 0x0100 | Qt.ItemDataRole.DisplayRole
-C_COUNT_ICON = 0x0100 | Qt.ItemDataRole.DecorationRole
-C_COUNT_ALIGN = 0x0100 | Qt.ItemDataRole.TextAlignmentRole
-C_ACTIVE_ICON = 0x0200 | Qt.ItemDataRole.DecorationRole
-C_ACTIVE_TIP = 0x0200 | Qt.ItemDataRole.ToolTipRole
-C_STATUS_ICON = 0x0300 | Qt.ItemDataRole.DecorationRole
-C_STATUS_TIP = 0x0300 | Qt.ItemDataRole.ToolTipRole
+C_LABEL_TEXT = 0x0000 | Qt.ItemDataRole.DisplayRole
+C_LABEL_ICON = 0x0000 | Qt.ItemDataRole.DecorationRole
+C_LABEL_FONT = 0x0000 | Qt.ItemDataRole.FontRole
+C_COUNT_TEXT = 0x0100 | Qt.ItemDataRole.DisplayRole
+C_COUNT_ICON = 0x0100 | Qt.ItemDataRole.DecorationRole
+C_COUNT_ALIGN = 0x0100 | Qt.ItemDataRole.TextAlignmentRole
+C_ACTIVE_ICON = 0x0200 | Qt.ItemDataRole.DecorationRole
+C_ACTIVE_TIP = 0x0200 | Qt.ItemDataRole.ToolTipRole
+C_ACTIVE_ACCESS = 0x0200 | Qt.ItemDataRole.AccessibleTextRole
+C_STATUS_ICON = 0x0300 | Qt.ItemDataRole.DecorationRole
+C_STATUS_TIP = 0x0300 | Qt.ItemDataRole.ToolTipRole
+C_STATUS_ACCESS = 0x0300 | Qt.ItemDataRole.AccessibleTextRole
NODE_FLAGS = Qt.ItemFlag.ItemIsEnabled
NODE_FLAGS |= Qt.ItemFlag.ItemIsSelectable
@@ -150,13 +152,15 @@ class ProjectNode:
# Active
aText, aIcon = self._item.getActiveStatus()
- self._cache[C_ACTIVE_TIP] = aText
self._cache[C_ACTIVE_ICON] = aIcon
+ self._cache[C_ACTIVE_TIP] = aText
+ self._cache[C_ACTIVE_ACCESS] = aText
# Status
sText, sIcon = self._item.getImportStatus()
- self._cache[C_STATUS_TIP] = sText
self._cache[C_STATUS_ICON] = sIcon
+ self._cache[C_STATUS_TIP] = sText
+ self._cache[C_STATUS_ACCESS] = sText
return
diff --git a/novelwriter/core/novelmodel.py b/novelwriter/core/novelmodel.py
index e5b49b15..0447f526 100644
--- a/novelwriter/core/novelmodel.py
+++ b/novelwriter/core/novelmodel.py
@@ -46,6 +46,7 @@ R_TEXT = Qt.ItemDataRole.DisplayRole
R_ICON = Qt.ItemDataRole.DecorationRole
R_ALIGN = Qt.ItemDataRole.TextAlignmentRole
R_TIP = Qt.ItemDataRole.ToolTipRole
+R_ACCESS = Qt.ItemDataRole.AccessibleTextRole
R_HANDLE = 0xff01
R_KEY = 0xff02
@@ -217,6 +218,7 @@ class NovelModel(QAbstractTableModel):
text = ", ".join(refs)
data[C_FACTOR*2 | R_TEXT] = text
data[C_FACTOR*2 | R_TIP] = f"{self._extraLabel}: {text}"
+ data[C_FACTOR*2 | R_ACCESS] = f"{self._extraLabel}: {text}"
data[C_FACTOR*3 | R_ICON] = self._more
data[R_HANDLE] = handle
data[R_KEY] = key
diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py
index ac98cd83..ad071e3e 100644
--- a/novelwriter/gui/noveltree.py
+++ b/novelwriter/gui/noveltree.py
@@ -298,6 +298,7 @@ class GuiNovelToolBar(QWidget):
self.novelValue.setHandle(rootHandle)
SHARED.project.data.setLastHandle(rootHandle, "novel")
self.novelView.setCurrentNovel(rootHandle)
+ self.novelView.novelTree.setAccessibleName(self.novelValue.currentText())
return
def setLastColType(self, colType: nwNovelExtra, doRefresh: bool = True) -> None:
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index 11c8b909..4565e315 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -254,6 +254,7 @@ class GuiProjectToolBar(QWidget):
self.viewLabel.setFont(SHARED.theme.guiFontB)
self.viewLabel.setContentsMargins(0, 0, 0, 0)
self.viewLabel.setSizePolicy(QtSizeExpanding, QtSizeExpanding)
+ self.projTree.setAccessibleName(self.viewLabel.text())
# Quick Links
self.mQuick = QMenu(self)
diff --git a/novelwriter/gui/search.py b/novelwriter/gui/search.py
index 323b2200..51bf1a15 100644
--- a/novelwriter/gui/search.py
+++ b/novelwriter/gui/search.py
@@ -125,6 +125,7 @@ class GuiProjectSearch(QWidget):
self.searchResult.setAllColumnsShowFocus(True)
self.searchResult.itemDoubleClicked.connect(self._searchResultDoubleClicked)
self.searchResult.itemSelectionChanged.connect(self._searchResultSelected)
+ self.searchResult.setAccessibleName(self.viewLabel.text())
if header := self.searchResult.header():
header.setStretchLastSection(False)
From da698b946b36d062f0e4c848a1154fea15aacdb3 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 11 May 2025 19:03:59 +0200
Subject: [PATCH 16/16] Improve accessibility of settings forms and dialogs
---
novelwriter/dialogs/preferences.py | 1 +
novelwriter/dialogs/projectsettings.py | 3 +++
novelwriter/extensions/configlayout.py | 8 ++++++--
novelwriter/tools/manussettings.py | 1 +
novelwriter/tools/noveldetails.py | 1 +
5 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py
index f98374af..bf330101 100644
--- a/novelwriter/dialogs/preferences.py
+++ b/novelwriter/dialogs/preferences.py
@@ -80,6 +80,7 @@ class GuiPreferences(NDialog):
# SideBar
self.sidebar = NPagedSideBar(self)
self.sidebar.setLabelColor(SHARED.theme.helpText)
+ self.sidebar.setAccessibleName(self.titleLabel.text())
self.sidebar.buttonClicked.connect(self._sidebarClicked)
# Form
diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py
index 9c8190be..623be345 100644
--- a/novelwriter/dialogs/projectsettings.py
+++ b/novelwriter/dialogs/projectsettings.py
@@ -86,6 +86,7 @@ class GuiProjectSettings(NDialog):
# SideBar
self.sidebar = NPagedSideBar(self)
self.sidebar.setLabelColor(SHARED.theme.helpText)
+ self.sidebar.setAccessibleName(self.titleLabel.text())
self.sidebar.addButton(self.tr("Settings"), self.PAGE_SETTINGS)
self.sidebar.addButton(self.tr("Status"), self.PAGE_STATUS)
self.sidebar.addButton(self.tr("Importance"), self.PAGE_IMPORT)
@@ -349,6 +350,7 @@ class _StatusPage(NFixedPage):
self.listBox.setHeaderLabels([self.tr("Label"), self.tr("Usage")])
self.listBox.setColumnWidth(self.C_LABEL, wCol0)
self.listBox.setIndentation(0)
+ self.listBox.setAccessibleName(pageLabel)
self.listBox.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.listBox.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.listBox.itemSelectionChanged.connect(self._onSelectionChanged)
@@ -686,6 +688,7 @@ class _ReplacePage(NFixedPage):
self.listBox.setHeaderLabels([self.tr("Keyword"), self.tr("Replace With")])
self.listBox.setColumnWidth(self.C_KEY, wCol0)
self.listBox.setIndentation(0)
+ self.listBox.setAccessibleName(self.pageTitle.text())
self.listBox.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.listBox.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.listBox.itemSelectionChanged.connect(self._onSelectionChanged)
diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py
index 8183f9a9..b82a0997 100644
--- a/novelwriter/extensions/configlayout.py
+++ b/novelwriter/extensions/configlayout.py
@@ -211,7 +211,8 @@ class NScrollableForm(QScrollArea):
else:
qWidget = widget
- qLabel = QLabel(label or "", self)
+ text = label or ""
+ qLabel = QLabel(text, self)
qLabel.setIndent(self._indent)
qLabel.setBuddy(qWidget)
@@ -227,6 +228,7 @@ class NScrollableForm(QScrollArea):
row.addLayout(labelBox, stretch[0])
if editable:
self._editable[editable] = qHelp
+ text = f"{text}: {helpText}"
else:
row.addWidget(qLabel, stretch[0])
@@ -235,6 +237,7 @@ class NScrollableForm(QScrollArea):
box.addWidget(qWidget, 1)
box.addWidget(QLabel(unit, self), 0)
row.addLayout(box, stretch[1])
+ text = f"{text} Unit: {unit}"
elif isinstance(button, QAbstractButton):
box = QHBoxLayout()
box.addWidget(qWidget, 1)
@@ -243,10 +246,11 @@ class NScrollableForm(QScrollArea):
else:
row.addWidget(qWidget, stretch[1])
+ self._first = False
self._layout.addLayout(row)
if label:
self._index[label.strip()] = qWidget
- self._first = False
+ qLabel.setAccessibleName(text)
return
diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py
index 3fa9bb03..855f64ea 100644
--- a/novelwriter/tools/manussettings.py
+++ b/novelwriter/tools/manussettings.py
@@ -104,6 +104,7 @@ class GuiBuildSettings(NToolDialog):
# SideBar
self.sidebar = NPagedSideBar(self)
self.sidebar.setLabelColor(SHARED.theme.helpText)
+ self.sidebar.setAccessibleName(self.titleLabel.text())
self.sidebar.addLabel(self.tr("General"))
self.sidebar.addButton(self.tr("Selection"), self.OPT_FILTERS)
diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py
index fbbcbb5d..75580233 100644
--- a/novelwriter/tools/noveldetails.py
+++ b/novelwriter/tools/noveldetails.py
@@ -86,6 +86,7 @@ class GuiNovelDetails(NNonBlockingDialog):
# SideBar
self.sidebar = NPagedSideBar(self)
self.sidebar.setLabelColor(SHARED.theme.helpText)
+ self.sidebar.setAccessibleName(self.titleLabel.text())
self.sidebar.addButton(self.tr("Overview"), self.PAGE_OVERVIEW)
self.sidebar.addButton(self.tr("Contents"), self.PAGE_CONTENTS)
self.sidebar.setSelected(self.PAGE_OVERVIEW)