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)