From bb876b55fc6b8af889378406ede80be281544e15 Mon Sep 17 00:00:00 2001 From: Amber Flina Date: Thu, 11 Sep 2025 09:47:32 -0300 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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()