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()