From b093446020a38d5eb423cbeff0aa47ede2cdab5b Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Aug 2025 15:08:42 +0200 Subject: [PATCH 01/15] Fix Qt side crash when forwarding editor auto-complete keypresses (#2510) --- novelwriter/gui/doceditor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 846b6317..83ff8fe7 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -1080,6 +1080,7 @@ class GuiDocEditor(QPlainTextEdit): if (block := self._qDocument.findBlock(pos)).isValid(): text = block.text() + if text and text[0] in "@%" and added + removed == 1: # Only run on single character changes, or it will trigger # at unwanted times when other changes are made to the document @@ -1094,10 +1095,6 @@ class GuiDocEditor(QPlainTextEdit): point = self.cursorRect().bottomRight() self._completer.move(viewport.mapToGlobal(point)) self._completer.show() - else: - self._completer.close() - else: - self._completer.close() if self._doReplace and added == 1: cursor = self.textCursor() @@ -1121,7 +1118,7 @@ class GuiDocEditor(QPlainTextEdit): cursor.setPosition(check, QtMoveAnchor) cursor.setPosition(check + length, QtKeepAnchor) cursor.insertText(text) - self._completer.hide() + self._completer.close() return @pyqtSlot() @@ -2186,6 +2183,7 @@ class CommandCompleter(QMenu): ): super().keyPressEvent(event) elif isinstance(parent, GuiDocEditor): + self.close() # Close to release the event lock before forwarding the key press (#2510) parent.keyPressEvent(event) return From 33b3a71ecdc832bef47d5e168aeecd6e5a6aaf9a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Aug 2025 15:11:51 +0200 Subject: [PATCH 02/15] Make a minor performance improvement in the editor completer --- novelwriter/gui/doceditor.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 83ff8fe7..daf99c4c 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -2086,10 +2086,13 @@ class CommandCompleter(QMenu): called on every keystroke on a line starting with @ or %. """ + __slots__ = ("_parent",) + complete = pyqtSignal(int, int, str) def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) + self._parent = parent return def updateMetaText(self, text: str, pos: int) -> bool: @@ -2176,15 +2179,14 @@ class CommandCompleter(QMenu): def keyPressEvent(self, event: QKeyEvent) -> None: """Capture keypresses and forward most of them to the editor.""" - parent = self.parent() if event.key() in ( Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Return, Qt.Key.Key_Enter, Qt.Key.Key_Escape ): super().keyPressEvent(event) - elif isinstance(parent, GuiDocEditor): + else: self.close() # Close to release the event lock before forwarding the key press (#2510) - parent.keyPressEvent(event) + self._parent.keyPressEvent(event) return ## From d69d2234c4dc91abab9cadbca17ae82a98976fdf Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Aug 2025 15:23:33 +0200 Subject: [PATCH 03/15] Rename completer signal --- novelwriter/gui/doceditor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index daf99c4c..0ac4a992 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -151,7 +151,7 @@ class GuiDocEditor(QPlainTextEdit): # Completer self._completer = CommandCompleter(self) - self._completer.complete.connect(self._insertCompletion) + self._completer.insertText.connect(self._insertCompletion) # Create Custom Document self._qDocument = GuiTextDocument(self) @@ -2088,7 +2088,7 @@ class CommandCompleter(QMenu): __slots__ = ("_parent",) - complete = pyqtSignal(int, int, str) + insertText = pyqtSignal(int, int, str) def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -2195,7 +2195,7 @@ class CommandCompleter(QMenu): def _emitComplete(self, pos: int, length: int, value: str) -> None: """Emit the signal to indicate a selection has been made.""" - self.complete.emit(pos, length, value) + self.insertText.emit(pos, length, value) return From 08d2a5fa958af433e1fbb069c5166f262b614e98 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 31 Aug 2025 15:27:55 +0200 Subject: [PATCH 04/15] Fix linting errors from updated Ruff version --- tests/test_gui/test_gui_guimain.py | 8 ++++---- tests/test_gui/test_gui_theme.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 6fda9b6a..69c4da26 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -812,13 +812,13 @@ def testGuiMain_OpenClose(qtbot, monkeypatch, nwGUI, projPath, fncPath, mockRnd) # Handle broken index on project open nwGUI.closeProject() idxPath: Path = projPath / "meta" / nwFiles.INDEX_FILE - assert idxPath.read_text() != "{}" - idxPath.write_text("{}") - assert idxPath.read_text() == "{}" + assert idxPath.read_text(encoding="utf-8") != "{}" + idxPath.write_text("{}", encoding="utf-8") + assert idxPath.read_text(encoding="utf-8") == "{}" nwGUI.openProject(projPath) nwGUI.saveProject() - assert idxPath.read_text() != "{}" + assert idxPath.read_text(encoding="utf-8") != "{}" assert nwGUI.docEditor.docHandle == C.hSceneDoc assert nwGUI.docViewer.docHandle == C.hTitlePage diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 7b0f518d..2deb3464 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -164,14 +164,14 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): # =============== mockTheme: Path = tstPaths.cnfDir / "themes" / "test.conf" - mockTheme.write_text( + mockTheme.write_text(( "[Main]\n" "name = Test\n" "\n" "[Palette]\n" "window = 0, 0, 0\n" "text = 255, 255, 255\n" - ) + ), encoding="utf-8") mainTheme._availThemes["test"] = mockTheme CONFIG.guiTheme = "test" From bb876b55fc6b8af889378406ede80be281544e15 Mon Sep 17 00:00:00 2001 From: Amber Flina Date: Thu, 11 Sep 2025 09:47:32 -0300 Subject: [PATCH 05/15] Handle IMEs --- novelwriter/gui/doceditor.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 0ac4a992..7a0f1c3f 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -44,8 +44,8 @@ from PyQt6.QtCore import ( ) from PyQt6.QtGui import ( QAction, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeyEvent, - QKeySequence, QMouseEvent, QPalette, QPixmap, QResizeEvent, QShortcut, - QTextBlock, QTextCursor, QTextDocument, QTextOption + QKeySequence, QInputMethodEvent, QMouseEvent, QPalette, QPixmap, QResizeEvent, + QShortcut, QTextBlock, QTextCursor, QTextDocument, QTextOption, ) from PyQt6.QtWidgets import ( QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu, @@ -1017,6 +1017,29 @@ class GuiDocEditor(QPlainTextEdit): super().resizeEvent(event) return + def inputMethodEvent(self, event: QInputMethodEvent) -> None: + """Handle text being input from CJK input methods""" + super().inputMethodEvent(event) + if event.commitString(): + self.ensureCursorVisible() + if self._completer.isVisible(): + rect = self.cursorRect() + pos = self.mapToGlobal(rect.bottomLeft()) + self._completer.move(pos) + + def inputMethodQuery(self, query: Qt.InputMethodQuery): + """Adjust completion windows for CJK input methods to consider + the viewport margins. + """ + if query == Qt.InputMethodQuery.ImCursorRectangle: + rect = self.cursorRect() + vM = self.viewportMargins() + rect.translate(vM.left(), vM.top()) + + return rect + + return super().inputMethodQuery(query) + ## # Public Slots ## From bad03c45a0fc0f7a7f02d5b6275d7825f6369c9c Mon Sep 17 00:00:00 2001 From: Amber Flina Date: Thu, 11 Sep 2025 09:53:04 -0300 Subject: [PATCH 06/15] Linting --- novelwriter/gui/doceditor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 7a0f1c3f..13fc4798 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -39,8 +39,8 @@ from enum import Enum, IntFlag from time import time from PyQt6.QtCore import ( - QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal, - pyqtSlot + QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, QVariant, + pyqtSignal, pyqtSlot ) from PyQt6.QtGui import ( QAction, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeyEvent, @@ -1018,7 +1018,7 @@ class GuiDocEditor(QPlainTextEdit): return def inputMethodEvent(self, event: QInputMethodEvent) -> None: - """Handle text being input from CJK input methods""" + """Handle text being input from CJK input methods.""" super().inputMethodEvent(event) if event.commitString(): self.ensureCursorVisible() @@ -1027,7 +1027,7 @@ class GuiDocEditor(QPlainTextEdit): pos = self.mapToGlobal(rect.bottomLeft()) self._completer.move(pos) - def inputMethodQuery(self, query: Qt.InputMethodQuery): + def inputMethodQuery(self, query: Qt.InputMethodQuery) -> QVariant: """Adjust completion windows for CJK input methods to consider the viewport margins. """ From 8a01ffa832886b5eb6f53490605ab6ab4bda475f Mon Sep 17 00:00:00 2001 From: Amber Flina Date: Thu, 11 Sep 2025 16:32:10 -0300 Subject: [PATCH 07/15] Other two linters --- novelwriter/gui/doceditor.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 13fc4798..8d4f0060 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -39,13 +39,14 @@ from enum import Enum, IntFlag from time import time from PyQt6.QtCore import ( - QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, QVariant, - pyqtSignal, pyqtSlot + QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal, + pyqtSlot ) from PyQt6.QtGui import ( - QAction, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeyEvent, - QKeySequence, QInputMethodEvent, QMouseEvent, QPalette, QPixmap, QResizeEvent, - QShortcut, QTextBlock, QTextCursor, QTextDocument, QTextOption, + QAction, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, + QInputMethodEvent, QKeyEvent, QKeySequence, QMouseEvent, QPalette, QPixmap, + QResizeEvent, QShortcut, QTextBlock, QTextCursor, QTextDocument, + QTextOption ) from PyQt6.QtWidgets import ( QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu, @@ -1027,7 +1028,7 @@ class GuiDocEditor(QPlainTextEdit): pos = self.mapToGlobal(rect.bottomLeft()) self._completer.move(pos) - def inputMethodQuery(self, query: Qt.InputMethodQuery) -> QVariant: + def inputMethodQuery(self, query: Qt.InputMethodQuery) -> object: """Adjust completion windows for CJK input methods to consider the viewport margins. """ From 42f0d71ecfd5bc25df2a86abab382e23105dfea1 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:01:17 +0200 Subject: [PATCH 08/15] Clean up types --- novelwriter/gui/doceditor.py | 15 +++++++-------- novelwriter/types.py | 2 ++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 8d4f0060..e4dba082 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -39,8 +39,8 @@ from enum import Enum, IntFlag from time import time from PyQt6.QtCore import ( - QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal, - pyqtSlot + QObject, QPoint, QRect, QRegularExpression, QRunnable, Qt, QTimer, + QVariant, pyqtSignal, pyqtSlot ) from PyQt6.QtGui import ( QAction, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, @@ -75,8 +75,9 @@ from novelwriter.text.counting import standardCounter from novelwriter.tools.lipsum import GuiLipsum from novelwriter.types import ( QtAlignCenterTop, QtAlignJustify, QtAlignLeft, QtAlignLeftTop, - QtAlignRight, QtKeepAnchor, QtModCtrl, QtModNone, QtModShift, QtMouseLeft, - QtMoveAnchor, QtMoveLeft, QtMoveRight, QtScrollAlwaysOff, QtScrollAsNeeded + QtAlignRight, QtImCursorRectangle, QtKeepAnchor, QtModCtrl, QtModNone, + QtModShift, QtMouseLeft, QtMoveAnchor, QtMoveLeft, QtMoveRight, + QtScrollAlwaysOff, QtScrollAsNeeded ) logger = logging.getLogger(__name__) @@ -1028,17 +1029,15 @@ class GuiDocEditor(QPlainTextEdit): pos = self.mapToGlobal(rect.bottomLeft()) self._completer.move(pos) - def inputMethodQuery(self, query: Qt.InputMethodQuery) -> object: + def inputMethodQuery(self, query: Qt.InputMethodQuery) -> QRect | QVariant: """Adjust completion windows for CJK input methods to consider the viewport margins. """ - if query == Qt.InputMethodQuery.ImCursorRectangle: + if query == QtImCursorRectangle: rect = self.cursorRect() vM = self.viewportMargins() rect.translate(vM.left(), vM.top()) - return rect - return super().inputMethodQuery(query) ## diff --git a/novelwriter/types.py b/novelwriter/types.py index fb0faebd..f7f7a36d 100644 --- a/novelwriter/types.py +++ b/novelwriter/types.py @@ -110,6 +110,8 @@ QtMoveAnchor = QTextCursor.MoveMode.MoveAnchor QtMoveLeft = QTextCursor.MoveOperation.Left QtMoveRight = QTextCursor.MoveOperation.Right +QtImCursorRectangle = Qt.InputMethodQuery.ImCursorRectangle + # Size Policy QtSizeExpanding = QSizePolicy.Policy.Expanding From 410d8bfe1302fd2a09315248dc15340f465052db Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 13 Sep 2025 15:27:38 +0200 Subject: [PATCH 09/15] Add test coverage --- novelwriter/gui/doceditor.py | 22 +++++++++++++--------- tests/test_gui/test_gui_doceditor.py | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index e4dba082..d93b9927 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -916,7 +916,7 @@ class GuiDocEditor(QPlainTextEdit): return True ## - # Document Events and Maintenance + # Events and Overloads ## def keyPressEvent(self, event: QKeyEvent) -> None: @@ -1023,19 +1023,18 @@ class GuiDocEditor(QPlainTextEdit): """Handle text being input from CJK input methods.""" super().inputMethodEvent(event) if event.commitString(): + # See issues #2267 and #2517 self.ensureCursorVisible() - if self._completer.isVisible(): - rect = self.cursorRect() - pos = self.mapToGlobal(rect.bottomLeft()) - self._completer.move(pos) + self._completerToCursor() def inputMethodQuery(self, query: Qt.InputMethodQuery) -> QRect | QVariant: """Adjust completion windows for CJK input methods to consider the viewport margins. """ if query == QtImCursorRectangle: - rect = self.cursorRect() + # See issues #2267 and #2517 vM = self.viewportMargins() + rect = self.cursorRect() rect.translate(vM.left(), vM.top()) return rect return super().inputMethodQuery(query) @@ -1109,15 +1108,14 @@ class GuiDocEditor(QPlainTextEdit): # at unwanted times when other changes are made to the document cursor = self.textCursor() bPos = cursor.positionInBlock() - if bPos > 0 and (viewport := self.viewport()): + if bPos > 0: if text[0] == "@": show = self._completer.updateMetaText(text, bPos) else: show = self._completer.updateCommentText(text, bPos) if show: - point = self.cursorRect().bottomRight() - self._completer.move(viewport.mapToGlobal(point)) self._completer.show() + self._completerToCursor() if self._doReplace and added == 1: cursor = self.textCursor() @@ -1918,6 +1916,12 @@ class GuiDocEditor(QPlainTextEdit): # Internal Functions ## + def _completerToCursor(self) -> None: + """Make sure the completer menu is positioned by the cursor.""" + if self._completer.isVisible() and (viewport := self.viewport()): + point = self.cursorRect().bottomLeft() + self._completer.move(viewport.mapToGlobal(point)) + def _correctWord(self, cursor: QTextCursor, word: str) -> None: """Slot for the spell check context menu triggering the replacement of a word with the word from the dictionary. diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 5bda2d82..08fbef0b 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -27,7 +27,8 @@ import pytest from PyQt6.QtCore import QEvent, QMimeData, QPointF, Qt, QThreadPool, QUrl from PyQt6.QtGui import ( QAction, QClipboard, QDesktopServices, QDragEnterEvent, QDragMoveEvent, - QDropEvent, QFont, QMouseEvent, QTextBlock, QTextCursor, QTextOption + QDropEvent, QFont, QInputMethodEvent, QMouseEvent, QTextBlock, QTextCursor, + QTextOption ) from PyQt6.QtWidgets import QApplication, QMenu, QPlainTextEdit @@ -1941,6 +1942,19 @@ def testGuiEditor_Completer(qtbot, nwGUI, projPath, mockRnd): "%Note.Consistency: \n" ) + # CJK completer reposition (#2267) + qtbot.keyClick(docEditor, "%", delay=KEY_DELAY) + assert completer.isVisible() is True + completer.move(0, 0) + assert completer.pos().x() == 0 + assert completer.pos().y() == 0 + + event = QInputMethodEvent() + event.setCommitString("Ping") + docEditor.inputMethodEvent(event) + assert completer.pos().x() > 0 # Should have moved + assert completer.pos().y() > 0 # Should have moved + # qtbot.stop() From da6b9b098046c0d3c841d6f5e900fdfccff4d6e2 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 13 Sep 2025 15:48:33 +0200 Subject: [PATCH 10/15] Add more comments --- tests/test_gui/test_gui_doceditor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 08fbef0b..4e240295 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -1942,18 +1942,18 @@ def testGuiEditor_Completer(qtbot, nwGUI, projPath, mockRnd): "%Note.Consistency: \n" ) - # CJK completer reposition (#2267) + # CJK completer reposition (#2267 and #2517) qtbot.keyClick(docEditor, "%", delay=KEY_DELAY) assert completer.isVisible() is True completer.move(0, 0) - assert completer.pos().x() == 0 - assert completer.pos().y() == 0 + assert completer.pos().x() == 0 # Completer menu at 0 + assert completer.pos().y() == 0 # Completer menu at 0 event = QInputMethodEvent() - event.setCommitString("Ping") + event.setCommitString("Text") docEditor.inputMethodEvent(event) - assert completer.pos().x() > 0 # Should have moved - assert completer.pos().y() > 0 # Should have moved + assert completer.pos().x() > 0 # Completer should have moved + assert completer.pos().y() > 0 # Completer should have moved # qtbot.stop() From c1d133035091e48845e20fa4128555f5e2c13236 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 14 Sep 2025 17:37:58 +0200 Subject: [PATCH 11/15] Bump version and update changelog --- CHANGELOG.md | 23 +++++++++++++++++++++++ novelwriter/__init__.py | 6 +++--- sample/nwProject.nwx | 4 ++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dc17163..d68344bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # novelWriter Changelog +## Version 2.7.5 [2025-09-14] + +### Release Notes + +This is a patch release that fixes an issue related to crashes when using the completer menu under +certain conditions, and improves positioning of the input box for CJK languages. + +### Detailed Changelog + +**Bugfixes** + +* Fixes an issue where the app would crash of deleting the `@` character with the completer menu + visible and the text margins of the editor set to "justified". This is likely crashing due to + some unhandled corner case in the Qt library, but the implementation of the completer menu in + novelWriter uses a small hack to bypass some intended behaviour of the menu. Extra steps have + been added to the implementation that seems to avoid the crash. Issue #2510. PR #2511. +* Fixes an issue where the input box that shows up when typing CJK languages were covering the text + due to an incorrect offset of the box location. The incorrect offset is caused by the text + margins not being taken into account. Fix by @Euophrys based on solution by @Jack-name. + Issues #2267 and #2517. PR #2518. + +---- + ## Version 2.7.4 [2025-07-15] ### Release Notes diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index fb4432e8..65a5c286 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -49,9 +49,9 @@ __license__ = "GPLv3" __author__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" -__version__ = "2.7.4" -__hexversion__ = "0x020704f0" -__date__ = "2025-07-15" +__version__ = "2.7.5" +__hexversion__ = "0x020705f0" +__date__ = "2025-09-14" __status__ = "Stable" __domain__ = "novelwriter.io" diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 3c7296dc..a4871a50 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Jane Smith From bb01d0e5aed2ff927626c07269ff84d188e8dd4d Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 14 Sep 2025 18:37:23 +0200 Subject: [PATCH 12/15] Fix debian build --- ...E-Apache-2.0.txt => LICENSE-Apache-2.0.txt | 0 pkgutils.py | 3 +-- pyproject.toml | 5 +---- utils/build_debian.py | 15 +++++++------- utils/common.py | 20 +++++++++++-------- 5 files changed, 22 insertions(+), 21 deletions(-) rename setup/LICENSE-Apache-2.0.txt => LICENSE-Apache-2.0.txt (100%) diff --git a/setup/LICENSE-Apache-2.0.txt b/LICENSE-Apache-2.0.txt similarity index 100% rename from setup/LICENSE-Apache-2.0.txt rename to LICENSE-Apache-2.0.txt diff --git a/pkgutils.py b/pkgutils.py index fd9ecf67..f71ea2f2 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -267,8 +267,7 @@ if __name__ == "__main__": cmdBuildUbuntu = parsers.add_parser( "build-ubuntu", help=( "Build a .deb package for Debian and Ubuntu. " - "Add --sign to sign package. " - "Add --first to set build number to 0." + "Add --sign to sign package." ) ) cmdBuildUbuntu.add_argument("--sign", action="store_true", help="Sign the package.") diff --git a/pyproject.toml b/pyproject.toml index 3610e012..9e4cf021 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,10 +10,7 @@ authors = [ description = "A plain text editor for planning and writing novels" readme = {file = "setup/description_pypi.md", content-type = "text/markdown"} license = "GPL-3.0-or-later AND Apache-2.0 AND CC-BY-4.0" -license-files = [ - "LICENSE.md", - "setup/LICENSE-Apache-2.0.txt", -] +license-files = ["LICENSE.md", "LICENSE-Apache-2.0.txt"] classifiers = [ "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", diff --git a/utils/build_debian.py b/utils/build_debian.py index e555ff95..a8cf9a05 100644 --- a/utils/build_debian.py +++ b/utils/build_debian.py @@ -36,7 +36,7 @@ SIGN_KEY = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08" def makeDebianPackage( signKey: str | None = None, sourceBuild: bool = False, distName: str = "unstable", - buildName: str = "", forLaunchpad: bool = False + buildName: str = "", forLaunchpad: bool = False, oldLicense: bool = False, ) -> str: """Build a Debian package.""" print("") @@ -96,7 +96,7 @@ def makeDebianPackage( print("Copying or generating additional files ...") print("") - copyPackageFiles(outDir, setupPy=True) + copyPackageFiles(outDir, oldLicense=oldLicense) # Copy/Write Debian Files # ======================= @@ -181,14 +181,14 @@ def launchpad(args: argparse.Namespace) -> None: bldNum = "0" distLoop = [ - ("24.04", "noble"), - ("25.04", "plucky"), - ("25.10", "questing"), + ("24.04", "noble", True), + ("25.04", "plucky", True), + ("25.10", "questing", False), ] print("Building Ubuntu packages for:") print("") - for distNum, codeName in distLoop: + for distNum, codeName, _ in distLoop: print(f" * Ubuntu {distNum} {codeName.title()}") print("") @@ -198,7 +198,7 @@ def launchpad(args: argparse.Namespace) -> None: print("") dputCmd = [] - for distNum, codeName in distLoop: + for distNum, codeName, oldLicense in distLoop: buildName = f"ubuntu{distNum}.{bldNum}" dCmd = makeDebianPackage( signKey=signKey, @@ -206,6 +206,7 @@ def launchpad(args: argparse.Namespace) -> None: distName=codeName, buildName=buildName, forLaunchpad=True, + oldLicense=oldLicense, ) dputCmd.append(dCmd) diff --git a/utils/common.py b/utils/common.py index 1784ed01..95dcac28 100644 --- a/utils/common.py +++ b/utils/common.py @@ -91,27 +91,31 @@ def copySourceCode(dst: Path) -> None: return -def copyPackageFiles(dst: Path, setupPy: bool = False) -> None: +def copyPackageFiles(dst: Path, oldLicense: bool = False) -> None: """Copy files needed for packaging.""" - copyFiles = ["LICENSE.md", "CREDITS.md", "pyproject.toml"] + copyFiles = ["LICENSE.md", "LICENSE-Apache-2.0.txt", "CREDITS.md", "pyproject.toml"] for copyFile in copyFiles: shutil.copyfile(copyFile, dst / copyFile) print("Copied:", copyFile, flush=True) writeFile(dst / "MANIFEST.in", ( "include LICENSE.md\n" + "include LICENSE-Apache-2.0.txt\n" "include CREDITS.md\n" "recursive-include novelwriter/assets *\n" )) - if setupPy: - writeFile(dst / "setup.py", ( - "import setuptools\n" - "setuptools.setup()\n" - )) - text = readFile(ROOT_DIR / "pyproject.toml") text = text.replace("setup/description_pypi.md", "data/description_short.txt") + if oldLicense: + new = [] + for line in text.splitlines(): + if line.startswith("license = "): + line = 'license = {text = "GPL-3.0-or-later AND Apache-2.0 AND CC-BY-4.0"}' + if line.startswith("license-files = "): + continue + new.append(line) + text = "\n".join(new) writeFile(dst / "pyproject.toml", text) return From a37482aaad009ff95e4b91f48f91bde50c188203 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 14 Sep 2025 18:54:42 +0200 Subject: [PATCH 13/15] Pin windows build to 2022 image --- .github/workflows/build_win.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_win.yml b/.github/workflows/build_win.yml index b9e96e9d..bee832c7 100644 --- a/.github/workflows/build_win.yml +++ b/.github/workflows/build_win.yml @@ -8,7 +8,7 @@ jobs: buildWin64: needs: buildAssets - runs-on: windows-latest + runs-on: windows-2022 steps: - name: Python Setup uses: actions/setup-python@v5 From f019a3cd0c42cef017ae5099effee5e68040c9b4 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 14 Sep 2025 19:24:32 +0200 Subject: [PATCH 14/15] Move the Apache license file back to setup --- pyproject.toml | 2 +- LICENSE-Apache-2.0.txt => setup/LICENSE-Apache-2.0.txt | 0 utils/common.py | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename LICENSE-Apache-2.0.txt => setup/LICENSE-Apache-2.0.txt (100%) diff --git a/pyproject.toml b/pyproject.toml index 9e4cf021..15736ca4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ description = "A plain text editor for planning and writing novels" readme = {file = "setup/description_pypi.md", content-type = "text/markdown"} license = "GPL-3.0-or-later AND Apache-2.0 AND CC-BY-4.0" -license-files = ["LICENSE.md", "LICENSE-Apache-2.0.txt"] +license-files = ["LICENSE.md", "setup/LICENSE-Apache-2.0.txt"] classifiers = [ "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", diff --git a/LICENSE-Apache-2.0.txt b/setup/LICENSE-Apache-2.0.txt similarity index 100% rename from LICENSE-Apache-2.0.txt rename to setup/LICENSE-Apache-2.0.txt diff --git a/utils/common.py b/utils/common.py index 95dcac28..09d937b1 100644 --- a/utils/common.py +++ b/utils/common.py @@ -93,14 +93,14 @@ def copySourceCode(dst: Path) -> None: def copyPackageFiles(dst: Path, oldLicense: bool = False) -> None: """Copy files needed for packaging.""" - copyFiles = ["LICENSE.md", "LICENSE-Apache-2.0.txt", "CREDITS.md", "pyproject.toml"] + copyFiles = ["LICENSE.md", "setup/LICENSE-Apache-2.0.txt", "CREDITS.md", "pyproject.toml"] for copyFile in copyFiles: shutil.copyfile(copyFile, dst / copyFile) print("Copied:", copyFile, flush=True) writeFile(dst / "MANIFEST.in", ( "include LICENSE.md\n" - "include LICENSE-Apache-2.0.txt\n" + "include setup/LICENSE-Apache-2.0.txt\n" "include CREDITS.md\n" "recursive-include novelwriter/assets *\n" )) From 4d398f170de3ed85301b412b03c004e4fb49196c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 14 Sep 2025 19:42:36 +0200 Subject: [PATCH 15/15] Add back attribute lost when merging --- novelwriter/gui/doceditor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index d35cb6b4..c6773f24 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -2079,6 +2079,7 @@ class CommandCompleter(QMenu): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) + self._parent = parent def updateMetaText(self, text: str, pos: int) -> bool: """Update the menu options based on the line of text."""