diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 0ac4a992..d93b9927 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, pyqtSignal, - pyqtSlot + QObject, QPoint, QRect, QRegularExpression, QRunnable, Qt, QTimer, + QVariant, pyqtSignal, pyqtSlot ) from PyQt6.QtGui import ( - QAction, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeyEvent, - QKeySequence, 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, @@ -74,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__) @@ -914,7 +916,7 @@ class GuiDocEditor(QPlainTextEdit): return True ## - # Document Events and Maintenance + # Events and Overloads ## def keyPressEvent(self, event: QKeyEvent) -> None: @@ -1017,6 +1019,26 @@ 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(): + # See issues #2267 and #2517 + self.ensureCursorVisible() + 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: + # See issues #2267 and #2517 + vM = self.viewportMargins() + rect = self.cursorRect() + rect.translate(vM.left(), vM.top()) + return rect + return super().inputMethodQuery(query) + ## # Public Slots ## @@ -1086,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() @@ -1895,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/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 diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 5bda2d82..4e240295 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 and #2517) + qtbot.keyClick(docEditor, "%", delay=KEY_DELAY) + assert completer.isVisible() is True + completer.move(0, 0) + assert completer.pos().x() == 0 # Completer menu at 0 + assert completer.pos().y() == 0 # Completer menu at 0 + + event = QInputMethodEvent() + event.setCommitString("Text") + docEditor.inputMethodEvent(event) + assert completer.pos().x() > 0 # Completer should have moved + assert completer.pos().y() > 0 # Completer should have moved + # qtbot.stop()