summaryrefslogtreecommitdiffstats
path: root/src/plugins/platforms/cocoa
diff options
context:
space:
mode:
authorTor Arne Vestbø <tor.arne.vestbo@qt.io>2021-08-19 16:27:57 +0200
committerTor Arne Vestbø <tor.arne.vestbo@qt.io>2021-08-24 13:39:06 +0200
commitbae9aeacbeb2d05aaec5e9dd7d1656ff929eb5db (patch)
tree06284952bcb78749ebeab4e7e8a5d949d66f65bd /src/plugins/platforms/cocoa
parentf6b851837df08b3f2fc6a571a2cc4567471a2495 (diff)
macOS: Correctly compute marked and selected text range, and use for insertion
The NSTextInputClient protocol expects marked (composed) and selected text ranges to be relative to the document, not to the current editing block as Qt typically expects. Luckily we can use the absolute cursor position to compute an absolute offset that we can apply to any other positions, such as the selection. Now that we are computing the ranges correctly we can also use them during text insertion, when the incoming replacementRange is not valid. We then transform and sanitize the replacement range to the format that Qt expects for QInputMethodEvent::setCommitString(). Pick-to: 6.2 Change-Id: I4cb2f7c63adb92e407f38af05adce539c9bed7e2 Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
Diffstat (limited to 'src/plugins/platforms/cocoa')
-rw-r--r--src/plugins/platforms/cocoa/qnsview_complextext.mm128
1 files changed, 111 insertions, 17 deletions
diff --git a/src/plugins/platforms/cocoa/qnsview_complextext.mm b/src/plugins/platforms/cocoa/qnsview_complextext.mm
index 987e3548af..40b8974795 100644
--- a/src/plugins/platforms/cocoa/qnsview_complextext.mm
+++ b/src/plugins/platforms/cocoa/qnsview_complextext.mm
@@ -75,14 +75,62 @@
return;
}
- const bool isAttributedString = [text isKindOfClass:NSAttributedString.class];
- QString commitString = QString::fromNSString(isAttributedString ? [text string] : text);
-
QObject *focusObject = m_platformWindow->window()->focusObject();
if (queryInputMethod(focusObject)) {
- QInputMethodEvent e;
- e.setCommitString(commitString);
- QCoreApplication::sendEvent(focusObject, &e);
+ QInputMethodEvent inputMethodEvent;
+
+ const bool isAttributedString = [text isKindOfClass:NSAttributedString.class];
+ QString commitString = QString::fromNSString(isAttributedString ? [text string] : text);
+
+ const auto markedRange = [self markedRange];
+ const auto selectedRange = [self selectedRange];
+
+ // If the replacement range is not specified we are expected to compute
+ // the range ourselves, based on the current state of the input context.
+ if (replacementRange.location == NSNotFound) {
+ if (markedRange.location != NSNotFound)
+ replacementRange = markedRange;
+ else
+ replacementRange = selectedRange;
+ }
+
+ // Qt's QInputMethodEvent has different semantics for the replacement
+ // range than AppKit does, so we need to sanitize the range first.
+ long long replaceFrom = replacementRange.location;
+ long long replaceLength = replacementRange.length;
+
+ // The QInputMethodEvent replacement start is relative to the start
+ // of the marked text (the location of the preedit string).
+ if (markedRange.location != NSNotFound)
+ replaceFrom -= markedRange.location;
+ else
+ replaceFrom = 0;
+
+ // The replacement length of QInputMethodEvent already includes
+ // the selection, as the documentation says that "If the widget
+ // has selected text, the selected text should get removed."
+ replaceLength -= selectedRange.length;
+
+ // The replacement length of QInputMethodEvent already includes
+ // the preedit string, as the documentation says that "When doing
+ // replacement, the area of the preedit string is ignored".
+ replaceLength -= markedRange.length;
+
+ // What we're left with is any _additional_ replacement.
+ // Make sure it's valid before passing it on.
+ replaceLength = qMax(0ll, replaceLength);
+
+ if (replaceFrom == NSNotFound) {
+ qCWarning(lcQpaKeys) << "Failed to compute valid replacement range for text insertion";
+ inputMethodEvent.setCommitString(commitString);
+ } else {
+ qCDebug(lcQpaKeys) << "Replacing from" << replaceFrom << "with length" << replaceLength
+ << "based on marked range" << markedRange << "and selection" << selectedRange;
+ inputMethodEvent.setCommitString(commitString, replaceFrom, replaceLength);
+ }
+
+ QCoreApplication::sendEvent(focusObject, &inputMethodEvent);
+
// prevent handleKeyEvent from sending a key event
m_sendKeyEvent = false;
}
@@ -227,17 +275,34 @@
return !m_composingText.isEmpty();
}
+/*
+ Returns the range of marked text or {cursorPosition, 0} if there's none.
+
+ This maps to the location and length of the current preedit (composited) string.
+
+ The returned range measures from the start of the receiver’s text storage,
+ that is, from 0 to the document length.
+*/
- (NSRange)markedRange
{
- NSRange range;
- if (!m_composingText.isEmpty()) {
- range.location = 0;
- range.length = m_composingText.length();
+ QObject *focusObject = m_platformWindow->window()->focusObject();
+ if (auto queryResult = queryInputMethod(focusObject, Qt::ImAbsolutePosition)) {
+ int absoluteCursorPosition = queryResult.value(Qt::ImAbsolutePosition).toInt();
+
+ // The cursor position as reflected by Qt::ImAbsolutePosition is not
+ // affected by the offset of the cursor in the preedit area. That means
+ // that when composing text, the cursor position stays the same, at the
+ // preedit insertion point, regardless of where the cursor is positioned within
+ // the preedit string by the QInputMethodEvent::Cursor attribute. This means
+ // we can use the cursor position to determine the range of the marked text.
+
+ // The NSTextInputClient documentation says {NSNotFound, 0} should be returned if there
+ // is no marked text, but in practice NSTextView seems to report {cursorPosition, 0},
+ // so we do the same.
+ return NSMakeRange(absoluteCursorPosition, m_composingText.length());
} else {
- range.location = NSNotFound;
- range.length = 0;
+ return {NSNotFound, 0};
}
- return range;
}
/*
@@ -302,14 +367,43 @@
// ------------- Various text properties -------------
+/*
+ Returns the range of selected text, or {cursorPosition, 0} if there's none.
+
+ The returned range measures from the start of the receiver’s text storage,
+ that is, from 0 to the document length.
+*/
- (NSRange)selectedRange
{
QObject *focusObject = m_platformWindow->window()->focusObject();
- if (auto queryResult = queryInputMethod(focusObject, Qt::ImCurrentSelection)) {
- QString selectedText = queryResult.value(Qt::ImCurrentSelection).toString();
- return selectedText.isEmpty() ? NSMakeRange(0, 0) : NSMakeRange(0, selectedText.length());
+ if (auto queryResult = queryInputMethod(focusObject,
+ Qt::ImCursorPosition | Qt::ImAbsolutePosition | Qt::ImAnchorPosition)) {
+
+ // Unfortunately the Qt::InputMethodQuery values are all relative
+ // to the start of the current editing block (paragraph), but we
+ // need them in absolute values relative to the entire text.
+ // Luckily we have one property, Qt::ImAbsolutePosition, that
+ // we can use to compute the offset.
+ int cursorPosition = queryResult.value(Qt::ImCursorPosition).toInt();
+ int absoluteCursorPosition = queryResult.value(Qt::ImAbsolutePosition).toInt();
+ int absoluteOffset = absoluteCursorPosition - cursorPosition;
+
+ int anchorPosition = absoluteOffset + queryResult.value(Qt::ImAnchorPosition).toInt();
+ int selectionStart = anchorPosition >= absoluteCursorPosition ? absoluteCursorPosition : anchorPosition;
+ int selectionEnd = selectionStart == anchorPosition ? absoluteCursorPosition : anchorPosition;
+ int selectionLength = selectionEnd - selectionStart;
+
+ // Note: The cursor position as reflected by these properties are not
+ // affected by the offset of the cursor in the preedit area. That means
+ // that when composing text, the cursor position stays the same, at the
+ // preedit insertion point, regardless of where the cursor is positioned within
+ // the preedit string by the QInputMethodEvent::Cursor attribute.
+
+ // The NSTextInputClient documentation says {NSNotFound, 0} should be returned if there is no
+ // selection, but in practice NSTextView seems to report {cursorPosition, 0}, so we do the same.
+ return NSMakeRange(selectionStart, selectionLength);
} else {
- return NSMakeRange(0, 0);
+ return {NSNotFound, 0};
}
}