From 9356a2a5dc19fa98e8b6582528e1593802022d46 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 22 Jul 2026 01:14:24 +0200 Subject: [PATCH 1/3] Reset daily writing progress when the previous progress was from another day --- novelwriter/core/projectdata.py | 22 ++++++++++--------- tests/core/test_projectdata.py | 39 +++++++++++++++++++++++++++++++++ tests/core/test_projectxml.py | 5 ++++- 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py index 0bc7c3e51..624c2d8b6 100644 --- a/novelwriter/core/projectdata.py +++ b/novelwriter/core/projectdata.py @@ -351,24 +351,26 @@ def setDailyTarget(self, value: Any, auto: Any) -> None: self._dailyGoalAuto = checkBool(auto, False) self._project.setProjectChanged(True) - def setDailyTargetCurrent(self, value: Any, date: Any) -> None: + def setDailyTargetCurrent(self, value: Any, last: Any) -> None: """Set the current daily goal.""" - self._dailyLastCount = checkInt(value, self._dailyLastCount) - self._dailyLastDate = checkDateNone(date, None) + if (lastDate := checkDateNone(last, None)) == date.today(): + self._dailyLastCount = checkInt(value, self._dailyLastCount) + self._dailyLastDate = lastDate + else: + self._dailyLastCount = 0 + self._dailyLastDate = date.today() def setDailyProgress(self, count: int) -> None: """Set the current daily goal progress.""" if self._dailyLastDate != date.today(): - # The date has changed since the last update, either because - # this is the first update ever, or because the clock has - # ticked over to a new day. Roll the last count back to what - # it was at the start of the new day. + # The date has changed since the last update or was not set. + # In either case, we shift to a new day with new counts. self._dailyLastCount -= self._dailyProgress self._dailyLastDate = date.today() - initial = self._initCounts[0] - self._dailyLastCount - self._dailyProgress = count - initial - self._remainingWordCount = self._targetWordCount - initial + offset = self._initCounts[0] - self._dailyLastCount + self._dailyProgress = count - offset + self._remainingWordCount = self._targetWordCount - offset def setLanguage(self, value: str | None) -> None: """Set the project language.""" diff --git a/tests/core/test_projectdata.py b/tests/core/test_projectdata.py index 54bca6b33..e2a962e0c 100644 --- a/tests/core/test_projectdata.py +++ b/tests/core/test_projectdata.py @@ -148,6 +148,44 @@ def testProjectData_AutoReplace(mockGUI): assert data.autoReplace == {"A": "B"} +@pytest.mark.core +def testProjectData_DailyTargetCurrent(monkeypatch, mockGUI): + """Test the setDailyTargetCurrent setter. A stored reference count + must only be restored when it was recorded on the same day; a count + from any other day is stale and must be discarded so it cannot leak + into today's progress calculation. + """ + monkeypatch.setattr(projectdata, "date", _FakeDate) + _FakeDate._today = date(2026, 7, 22) + + project = MockProject() + data = ProjectData(project) # type: ignore + + # A record from the same day is restored as-is + data.setDailyTargetCurrent(60, "2026-07-22") + assert data.dailyLastCount == 60 + assert data._dailyLastDate == date(2026, 7, 22) + + # A record from an earlier day is stale and discarded, and the date + # is bumped to today so it isn't mistaken for a fresh record later + data.setDailyTargetCurrent(100, "2026-07-20") + assert data.dailyLastCount == 0 + assert data._dailyLastDate == date(2026, 7, 22) + + # A missing/invalid date is treated the same as a stale record + data.setDailyTargetCurrent(100, None) + assert data.dailyLastCount == 0 + assert data._dailyLastDate == date(2026, 7, 22) + + # End-to-end: a project file with a daily target set two days ago is + # loaded. The stale count must not offset today's progress + data = ProjectData(project) # type: ignore + data.setInitCounts(wNovel=500) + data.setDailyTargetCurrent(100, "2026-07-20") + data.setDailyProgress(500) + assert data.dailyProgress == 0 + + @pytest.mark.core def testProjectData_DailyProgress(monkeypatch, mockGUI): """Test the setDailyProgress setter. The daily progress and the @@ -156,6 +194,7 @@ def testProjectData_DailyProgress(monkeypatch, mockGUI): new day. """ monkeypatch.setattr(projectdata, "date", _FakeDate) + _FakeDate._today = date(2026, 7, 20) project = MockProject() data = ProjectData(project) # type: ignore diff --git a/tests/core/test_projectxml.py b/tests/core/test_projectxml.py index 75636d9f9..16545bcb5 100644 --- a/tests/core/test_projectxml.py +++ b/tests/core/test_projectxml.py @@ -170,7 +170,10 @@ def testProjectXMLReader_ReadCurrent(monkeypatch, mockGUI, tstPaths, fncPath): assert data.targetDeadline == date.fromisoformat("2026-08-20") assert data.dailyGoal == 1000 assert data.dailyGoalAuto is False - assert data._dailyLastDate == date.fromisoformat("2026-07-20") + # The fixture's daily target date (2026-07-20) is stale by the time this + # test runs, so the loaded reference count must be discarded and the + # date bumped to today rather than kept as the stale loaded value + assert data._dailyLastDate == date.today() assert data._dailyLastCount == 0 assert data.itemStatus["sf12341"].name == "New" From 6d180adca9fc5c410355dae9a8c5391963fe14a6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 22 Jul 2026 01:23:15 +0200 Subject: [PATCH 2/3] Make sure text is coped as plain text from the editor --- novelwriter/editor/editor.py | 8 ++++++++ tests/editor/test_editor.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/novelwriter/editor/editor.py b/novelwriter/editor/editor.py index db1168d7a..50879061e 100644 --- a/novelwriter/editor/editor.py +++ b/novelwriter/editor/editor.py @@ -1412,6 +1412,14 @@ def inputMethodQuery(self, query: Qt.InputMethodQuery) -> QRect | QVariant: return variant return super().inputMethodQuery(query) + def createMimeDataFromSelection(self) -> QMimeData: + """Overload mime data creation for copy/cut so that only plain + text is put on the clipboard, and not also HTML. + """ + data = QMimeData() + data.setText(self.getSelectedText()) + return data + def insertFromMimeData(self, source: QMimeData | None) -> None: """Overload mime data insertion in the document.""" if not source: diff --git a/tests/editor/test_editor.py b/tests/editor/test_editor.py index 78d80ce4b..30360a206 100644 --- a/tests/editor/test_editor.py +++ b/tests/editor/test_editor.py @@ -2719,6 +2719,14 @@ def testGuiDocEditor_InternalSlotEdgeCases(qtbot, nwGUI, projPath, mockRnd): cursor = docEditor._autoSelect() assert cursor.selectedText() != "" + # Copying a selection must only put plain text on the clipboard, + # not also HTML, regardless of the widget's rich text rendering + docEditor.setPlainText("first\n\nsecond") + docEditor.docAction(nwDocAction.SEL_ALL) + mime = docEditor.createMimeDataFromSelection() + assert mime.hasHtml() is False + assert mime.text() == "first\n\nsecond" + @pytest.mark.gui def testGuiDocEditor_UpdateDocMargins(qtbot, nwGUI, projPath, mockRnd): From f6e4d27272c1a46210c210a49c530d033af2d596 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 22 Jul 2026 01:32:03 +0200 Subject: [PATCH 3/3] Fix leading whitespace stripped on paste --- novelwriter/editor/editor.py | 2 +- tests/editor/test_editor.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/novelwriter/editor/editor.py b/novelwriter/editor/editor.py index 50879061e..6a7b86ba5 100644 --- a/novelwriter/editor/editor.py +++ b/novelwriter/editor/editor.py @@ -1438,7 +1438,7 @@ def insertFromMimeData(self, source: QMimeData | None) -> None: document.setMarkdown(data) if document is not None: - text = FromQTextDocument(document).convertText().strip() + text = FromQTextDocument(document).convertText().strip("\n") elif source.hasText(): text = source.text() else: diff --git a/tests/editor/test_editor.py b/tests/editor/test_editor.py index 30360a206..746e3e001 100644 --- a/tests/editor/test_editor.py +++ b/tests/editor/test_editor.py @@ -2885,6 +2885,20 @@ def testGuiDocEditor_InsertFromMimeData_Markdown(qtbot, nwGUI, projPath, mockRnd assert docEditor.getText() == text + # A rich text (HTML) paste with a meaningful leading space (as a + # non-breaking space, which is how rich text sources encode a + # leading space that would otherwise collapse in HTML) must only + # have the blank line added by the HTML-to-text conversion + # stripped, not the leading space itself + docEditor.replaceText("A cat.") + docEditor.setCursorPosition(1) + + htmlMime = QMimeData() + htmlMime.setHtml("

 big

") + docEditor.insertFromMimeData(htmlMime) + + assert docEditor.getText().startswith("A\u00a0big cat.") + @pytest.mark.gui def testGuiDocEditor_LineHeightDoubleReturn(qtbot, nwGUI, projPath, mockRnd):