QAndroidInputContext: Improve compatibility with virtual keyboards
This commit improves QAndroidInputContext's conformance to Android's InputConnection interface and/or consistency of it's behavior with Android's native EditText control. * Composing region is now completely independent from cursor and selection, as required by InputConnection documentation. Also, Qt will now never clear composing region (i.e. call finishComposingText()) without receiving a command to do so from the keyboard. This is important for the following reasons: - Some keyboards misbehave if we change composing region without receiving a command from them. Notably, Samsung Keyboard does (QTBUG-68822). - Due to asynchronous nature of interaction between QAndroidInputContext and the keyboard, when user drags cursor handle quickly, the keyboard may call setComposingRegion() to mark a word, which is no longer under the cursor. This was causing text corruption (QTBUG-43156, QTBUG-59958). Also SwiftKey makes such calls when user presses Enter key (QTBUG-57819). - For similar reasons selecting a word with a double-tap could cause text corruption. The keyboard may call setComposingRegion() in response to the first tap after the second tap has been processed and the word has already been already selected. This is achieved by keeping track of start and end of composing region independently from the editor. Whenever possible (i.e. when there is no selection and the cursor is inside composing region), the composing text is represented as preedit text inside editor. And whenever that is imposible, the editor is told to commit, but QAndroidInputContext keeps information about composing region internally to be able to correctly interract with the keyboard. * deleteSurroundingText() has been re-written to work correctly when there are selection and/or composing region. Some keyboards (e.g Ginger Keyboard) do call deleteSurroundingText() when there is non-empty composing region. * All operations are now performed inside a batch edit (i.e. QAndroidInputContext now calls beginBatchEdit() and endBatchEdit() on itself) to ensure that an intermediate state is never reported to the keyboard, whenever an operation requires more than one QInputMethodEvent. BatchEditLock helper class was added to call begin/endBatchEdit() in RAII style. m_blockUpdateSelection has been removed because m_batchEditNestingLevel is now used instead of it. * Selection start and end positions are now reported to the keyboard so that start <= end. Some keyboards can not handle start > end. * getTextBefore/AfterCursor() now exclude selected text from their return values. While Android docs say "text before/after cursor", what they really mean is "text before/after selection" because "the cursor and the selection are one and the same thing". Some keyboards (e.g. Gboard) were behaving incorrectly when selected text was being returned. * getExtractedText() now tries to obtain and return the whole text from the editor. This is to fix compatibility with some buggy keyboards (e.g. Samsung Keyboard, Minuum) that ignore startOffset field and assume that selectionStart and selectionEnd are absolute values. Then they issue commands with wrong indexes in some cases. Fixes: QTBUG-43156 Fixes: QTBUG-59958 Fixes: QTBUG-57819 Fixes: QTBUG-68822 Change-Id: I7e71f3bcfbb2c32248d653a4197293db03579a79 Reviewed-by: BogDan Vatra <bogdan@kdab.com>
This commit is contained in:
parent
e5f2be256f
commit
1ade5ea41a
@ -64,29 +64,33 @@
|
|||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
QT_BEGIN_NAMESPACE
|
||||||
|
|
||||||
template <typename T>
|
namespace {
|
||||||
class ScopedValueChangeBack
|
|
||||||
|
class BatchEditLock
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ScopedValueChangeBack(T &variable, T newValue)
|
|
||||||
: m_oldValue(variable),
|
explicit BatchEditLock(QAndroidInputContext *context)
|
||||||
m_variable(variable)
|
: m_context(context)
|
||||||
{
|
{
|
||||||
m_variable = newValue;
|
m_context->beginBatchEdit();
|
||||||
}
|
}
|
||||||
inline void setOldValue()
|
|
||||||
|
~BatchEditLock()
|
||||||
{
|
{
|
||||||
m_variable = m_oldValue;
|
m_context->endBatchEdit();
|
||||||
}
|
|
||||||
~ScopedValueChangeBack()
|
|
||||||
{
|
|
||||||
setOldValue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BatchEditLock(const BatchEditLock &) = delete;
|
||||||
|
BatchEditLock &operator=(const BatchEditLock &) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
T m_oldValue;
|
|
||||||
T &m_variable;
|
QAndroidInputContext *m_context;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace anonymous
|
||||||
|
|
||||||
static QAndroidInputContext *m_androidInputContext = 0;
|
static QAndroidInputContext *m_androidInputContext = 0;
|
||||||
static char const *const QtNativeInputConnectionClassName = "org/qtproject/qt5/android/QtNativeInputConnection";
|
static char const *const QtNativeInputConnectionClassName = "org/qtproject/qt5/android/QtNativeInputConnection";
|
||||||
static char const *const QtExtractedTextClassName = "org/qtproject/qt5/android/QtExtractedText";
|
static char const *const QtExtractedTextClassName = "org/qtproject/qt5/android/QtExtractedText";
|
||||||
@ -423,8 +427,12 @@ static QRect inputItemRectangle()
|
|||||||
}
|
}
|
||||||
|
|
||||||
QAndroidInputContext::QAndroidInputContext()
|
QAndroidInputContext::QAndroidInputContext()
|
||||||
: QPlatformInputContext(), m_composingTextStart(-1), m_blockUpdateSelection(false),
|
: QPlatformInputContext()
|
||||||
m_handleMode(Hidden), m_batchEditNestingLevel(0), m_focusObject(0)
|
, m_composingTextStart(-1)
|
||||||
|
, m_composingCursor(-1)
|
||||||
|
, m_handleMode(Hidden)
|
||||||
|
, m_batchEditNestingLevel(0)
|
||||||
|
, m_focusObject(0)
|
||||||
{
|
{
|
||||||
jclass clazz = QJNIEnvironmentPrivate::findClass(QtNativeInputConnectionClassName);
|
jclass clazz = QJNIEnvironmentPrivate::findClass(QtNativeInputConnectionClassName);
|
||||||
if (Q_UNLIKELY(!clazz)) {
|
if (Q_UNLIKELY(!clazz)) {
|
||||||
@ -565,13 +573,13 @@ void QAndroidInputContext::reset()
|
|||||||
|
|
||||||
void QAndroidInputContext::commit()
|
void QAndroidInputContext::commit()
|
||||||
{
|
{
|
||||||
finishComposingText();
|
focusObjectStopComposing();
|
||||||
}
|
}
|
||||||
|
|
||||||
void QAndroidInputContext::updateCursorPosition()
|
void QAndroidInputContext::updateCursorPosition()
|
||||||
{
|
{
|
||||||
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
|
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
|
||||||
if (!query.isNull() && !m_blockUpdateSelection && !m_batchEditNestingLevel) {
|
if (!query.isNull() && m_batchEditNestingLevel == 0) {
|
||||||
const int cursorPos = getAbsoluteCursorPosition(query);
|
const int cursorPos = getAbsoluteCursorPosition(query);
|
||||||
const int composeLength = m_composingText.length();
|
const int composeLength = m_composingText.length();
|
||||||
|
|
||||||
@ -579,24 +587,29 @@ void QAndroidInputContext::updateCursorPosition()
|
|||||||
if (m_composingText.isEmpty() != (m_composingTextStart == -1))
|
if (m_composingText.isEmpty() != (m_composingTextStart == -1))
|
||||||
qWarning() << "Input method out of sync" << m_composingText << m_composingTextStart;
|
qWarning() << "Input method out of sync" << m_composingText << m_composingTextStart;
|
||||||
|
|
||||||
int realCursorPosition = cursorPos;
|
int realSelectionStart = cursorPos;
|
||||||
int realAnchorPosition = cursorPos;
|
int realSelectionEnd = cursorPos;
|
||||||
|
|
||||||
int cpos = query->value(Qt::ImCursorPosition).toInt();
|
int cpos = query->value(Qt::ImCursorPosition).toInt();
|
||||||
int anchor = query->value(Qt::ImAnchorPosition).toInt();
|
int anchor = query->value(Qt::ImAnchorPosition).toInt();
|
||||||
if (cpos != anchor) {
|
if (cpos != anchor) {
|
||||||
if (!m_composingText.isEmpty()) {
|
if (!m_composingText.isEmpty()) {
|
||||||
qWarning("Selecting text while preediting may give unpredictable results.");
|
qWarning("Selecting text while preediting may give unpredictable results.");
|
||||||
finishComposingText();
|
focusObjectStopComposing();
|
||||||
}
|
}
|
||||||
int blockPos = getBlockPosition(query);
|
int blockPos = getBlockPosition(query);
|
||||||
realCursorPosition = blockPos + cpos;
|
realSelectionStart = blockPos + cpos;
|
||||||
realAnchorPosition = blockPos + anchor;
|
realSelectionEnd = blockPos + anchor;
|
||||||
}
|
}
|
||||||
// Qt's idea of the cursor position is the start of the preedit area, so we maintain our own preedit cursor pos
|
// Qt's idea of the cursor position is the start of the preedit area, so we maintain our own preedit cursor pos
|
||||||
if (!m_composingText.isEmpty())
|
if (focusObjectIsComposing())
|
||||||
realCursorPosition = realAnchorPosition = m_composingCursor;
|
realSelectionStart = realSelectionEnd = m_composingCursor;
|
||||||
QtAndroidInput::updateSelection(realCursorPosition, realAnchorPosition,
|
|
||||||
|
// Some keyboards misbahave when selStart > selEnd
|
||||||
|
if (realSelectionStart > realSelectionEnd)
|
||||||
|
std::swap(realSelectionStart, realSelectionEnd);
|
||||||
|
|
||||||
|
QtAndroidInput::updateSelection(realSelectionStart, realSelectionEnd,
|
||||||
m_composingTextStart, m_composingTextStart + composeLength); // pre-edit text
|
m_composingTextStart, m_composingTextStart + composeLength); // pre-edit text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -666,7 +679,7 @@ void QAndroidInputContext::updateSelectionHandles()
|
|||||||
*/
|
*/
|
||||||
void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y)
|
void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y)
|
||||||
{
|
{
|
||||||
if (m_batchEditNestingLevel.load() || m_blockUpdateSelection) {
|
if (m_batchEditNestingLevel != 0) {
|
||||||
qWarning() << "QAndroidInputContext::handleLocationChanged returned";
|
qWarning() << "QAndroidInputContext::handleLocationChanged returned";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -741,15 +754,15 @@ void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if handle has been dragged far enough
|
// Check if handle has been dragged far enough
|
||||||
if (m_composingText.isEmpty() && newCpos == cpos && newAnchor == anchor)
|
if (!focusObjectIsComposing() && newCpos == cpos && newAnchor == anchor)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
If there is composing text, we have to compare newCpos with m_composingCursor instead of cpos.
|
If the editor is currently in composing state, we have to compare newCpos with
|
||||||
And since there is nothing to compare with newAnchor, we perform the check only when user
|
m_composingCursor instead of cpos. And since there is nothing to compare with newAnchor, we
|
||||||
drags the cursor handle.
|
perform the check only when user drags the cursor handle.
|
||||||
*/
|
*/
|
||||||
if (!m_composingText.isEmpty() && handleId == 1) {
|
if (focusObjectIsComposing() && handleId == 1) {
|
||||||
int absoluteCpos = query.value(Qt::ImAbsolutePosition).toInt(&ok);
|
int absoluteCpos = query.value(Qt::ImAbsolutePosition).toInt(&ok);
|
||||||
if (!ok)
|
if (!ok)
|
||||||
absoluteCpos = cpos;
|
absoluteCpos = cpos;
|
||||||
@ -759,7 +772,9 @@ void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
finishComposingText();
|
BatchEditLock batchEditLock(this);
|
||||||
|
|
||||||
|
focusObjectStopComposing();
|
||||||
|
|
||||||
QList<QInputMethodEvent::Attribute> attributes;
|
QList<QInputMethodEvent::Attribute> attributes;
|
||||||
attributes.append({ QInputMethodEvent::Selection, newAnchor, newCpos - newAnchor });
|
attributes.append({ QInputMethodEvent::Selection, newAnchor, newCpos - newAnchor });
|
||||||
@ -777,7 +792,7 @@ void QAndroidInputContext::touchDown(int x, int y)
|
|||||||
m_handleMode = ShowCursor;
|
m_handleMode = ShowCursor;
|
||||||
// The VK will appear in a moment, stop the timer
|
// The VK will appear in a moment, stop the timer
|
||||||
m_hideCursorHandleTimer.stop();
|
m_hideCursorHandleTimer.stop();
|
||||||
finishComposingText();
|
focusObjectStopComposing();
|
||||||
updateSelectionHandles();
|
updateSelectionHandles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -789,13 +804,19 @@ void QAndroidInputContext::longPress(int x, int y)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (m_focusObject && inputItemRectangle().contains(x, y)) {
|
if (m_focusObject && inputItemRectangle().contains(x, y)) {
|
||||||
finishComposingText();
|
BatchEditLock batchEditLock(this);
|
||||||
|
|
||||||
|
focusObjectStopComposing();
|
||||||
|
|
||||||
// Release left button, otherwise the following events will cancel the menu popup
|
// Release left button, otherwise the following events will cancel the menu popup
|
||||||
QtAndroidInput::releaseMouse(x, y);
|
QtAndroidInput::releaseMouse(x, y);
|
||||||
|
|
||||||
handleLocationChanged(1, x, y);
|
const double pixelDensity =
|
||||||
ScopedValueChangeBack<bool> svcb(m_blockUpdateSelection, true);
|
QGuiApplication::focusWindow()
|
||||||
|
? QHighDpiScaling::factor(QGuiApplication::focusWindow())
|
||||||
|
: QHighDpiScaling::factor(QtAndroid::androidPlatformIntegration()->screen());
|
||||||
|
const QPointF touchPoint(x / pixelDensity, y / pixelDensity);
|
||||||
|
setSelectionOnFocusObject(touchPoint, touchPoint);
|
||||||
|
|
||||||
QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition | Qt::ImTextBeforeCursor | Qt::ImTextAfterCursor);
|
QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition | Qt::ImTextBeforeCursor | Qt::ImTextAfterCursor);
|
||||||
QCoreApplication::sendEvent(m_focusObject, &query);
|
QCoreApplication::sendEvent(m_focusObject, &query);
|
||||||
@ -934,6 +955,7 @@ void QAndroidInputContext::clear()
|
|||||||
{
|
{
|
||||||
m_composingText.clear();
|
m_composingText.clear();
|
||||||
m_composingTextStart = -1;
|
m_composingTextStart = -1;
|
||||||
|
m_composingCursor = -1;
|
||||||
m_extractedText.clear();
|
m_extractedText.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -941,9 +963,8 @@ void QAndroidInputContext::clear()
|
|||||||
void QAndroidInputContext::setFocusObject(QObject *object)
|
void QAndroidInputContext::setFocusObject(QObject *object)
|
||||||
{
|
{
|
||||||
if (object != m_focusObject) {
|
if (object != m_focusObject) {
|
||||||
|
focusObjectStopComposing();
|
||||||
m_focusObject = object;
|
m_focusObject = object;
|
||||||
if (!m_composingText.isEmpty())
|
|
||||||
finishComposingText();
|
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
QPlatformInputContext::setFocusObject(object);
|
QPlatformInputContext::setFocusObject(object);
|
||||||
@ -958,78 +979,135 @@ jboolean QAndroidInputContext::beginBatchEdit()
|
|||||||
|
|
||||||
jboolean QAndroidInputContext::endBatchEdit()
|
jboolean QAndroidInputContext::endBatchEdit()
|
||||||
{
|
{
|
||||||
if (--m_batchEditNestingLevel == 0 && !m_blockUpdateSelection) //ending batch edit mode
|
if (--m_batchEditNestingLevel == 0) { //ending batch edit mode
|
||||||
|
focusObjectStartComposing();
|
||||||
updateCursorPosition();
|
updateCursorPosition();
|
||||||
|
}
|
||||||
return JNI_TRUE;
|
return JNI_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Android docs say: If composing, replace compose text with \a text.
|
Android docs say: This behaves like calling setComposingText(text, newCursorPosition) then
|
||||||
Otherwise insert \a text at current cursor position.
|
finishComposingText().
|
||||||
|
|
||||||
The cursor should then be moved to newCursorPosition. If > 0, this is
|
|
||||||
relative to the end of the text - 1; if <= 0, this is relative to the start
|
|
||||||
of the text. updateSelection() needs to be called.
|
|
||||||
*/
|
*/
|
||||||
jboolean QAndroidInputContext::commitText(const QString &text, jint newCursorPosition)
|
jboolean QAndroidInputContext::commitText(const QString &text, jint newCursorPosition)
|
||||||
{
|
{
|
||||||
ScopedValueChangeBack<bool> svcb(m_blockUpdateSelection, true);
|
BatchEditLock batchEditLock(this);
|
||||||
QInputMethodEvent event;
|
return setComposingText(text, newCursorPosition) && finishComposingText();
|
||||||
event.setCommitString(text);
|
|
||||||
sendInputMethodEvent(&event);
|
|
||||||
clear();
|
|
||||||
|
|
||||||
// Qt has now put the cursor at the end of the text, corresponding to newCursorPosition == 1
|
|
||||||
if (newCursorPosition != 1) {
|
|
||||||
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
|
|
||||||
if (!query.isNull()) {
|
|
||||||
QList<QInputMethodEvent::Attribute> attributes;
|
|
||||||
const int localPos = query->value(Qt::ImCursorPosition).toInt();
|
|
||||||
const int newLocalPos = newCursorPosition > 0
|
|
||||||
? localPos + newCursorPosition - 1
|
|
||||||
: localPos - text.length() + newCursorPosition;
|
|
||||||
//move the cursor
|
|
||||||
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection,
|
|
||||||
newLocalPos, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
svcb.setOldValue();
|
|
||||||
updateCursorPosition();
|
|
||||||
return JNI_TRUE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint rightLength)
|
jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint rightLength)
|
||||||
{
|
{
|
||||||
|
BatchEditLock batchEditLock(this);
|
||||||
|
|
||||||
|
focusObjectStopComposing();
|
||||||
|
|
||||||
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
|
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
|
||||||
if (query.isNull())
|
if (query.isNull())
|
||||||
return JNI_TRUE;
|
return JNI_TRUE;
|
||||||
|
|
||||||
m_composingText.clear();
|
|
||||||
m_composingTextStart = -1;
|
|
||||||
|
|
||||||
if (leftLength < 0) {
|
if (leftLength < 0) {
|
||||||
rightLength += -leftLength;
|
rightLength += -leftLength;
|
||||||
leftLength = 0;
|
leftLength = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int initialBlockPos = getBlockPosition(query);
|
||||||
|
const int initialCursorPos = getAbsoluteCursorPosition(query);
|
||||||
|
const int initialAnchorPos = initialBlockPos + query->value(Qt::ImAnchorPosition).toInt();
|
||||||
|
|
||||||
|
/*
|
||||||
|
According to documentation, we should delete leftLength characters before current selection
|
||||||
|
and rightLength characters after current selection (without affecting selection). But that is
|
||||||
|
absolutely not what Android's native EditText does. It deletes leftLength characters before
|
||||||
|
min(selection start, composing region start) and rightLength characters after max(selection
|
||||||
|
end, composing region end). There are no known keyboards that depend on this behavior, but
|
||||||
|
it is better to be consistent with EditText behavior, because there definetly should be no
|
||||||
|
keyboards that depend on documented behavior.
|
||||||
|
*/
|
||||||
|
const int leftEnd =
|
||||||
|
m_composingText.isEmpty()
|
||||||
|
? qMin(initialCursorPos, initialAnchorPos)
|
||||||
|
: qMin(qMin(initialCursorPos, initialAnchorPos), m_composingTextStart);
|
||||||
|
|
||||||
|
const int rightBegin =
|
||||||
|
m_composingText.isEmpty()
|
||||||
|
? qMax(initialCursorPos, initialAnchorPos)
|
||||||
|
: qMax(qMax(initialCursorPos, initialAnchorPos),
|
||||||
|
m_composingTextStart + m_composingText.length());
|
||||||
|
|
||||||
|
int textBeforeCursorLen;
|
||||||
|
int textAfterCursorLen;
|
||||||
|
|
||||||
QVariant textBeforeCursor = query->value(Qt::ImTextBeforeCursor);
|
QVariant textBeforeCursor = query->value(Qt::ImTextBeforeCursor);
|
||||||
QVariant textAfterCursor = query->value(Qt::ImTextAfterCursor);
|
QVariant textAfterCursor = query->value(Qt::ImTextAfterCursor);
|
||||||
if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
|
if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
|
||||||
leftLength = qMin(leftLength, textBeforeCursor.toString().length());
|
textBeforeCursorLen = textBeforeCursor.toString().length();
|
||||||
rightLength = qMin(rightLength, textAfterCursor.toString().length());
|
textAfterCursorLen = textAfterCursor.toString().length();
|
||||||
} else {
|
} else {
|
||||||
int cursorPos = query->value(Qt::ImCursorPosition).toInt();
|
textBeforeCursorLen = initialCursorPos - initialBlockPos;
|
||||||
leftLength = qMin(leftLength, cursorPos);
|
textAfterCursorLen =
|
||||||
rightLength = qMin(rightLength, query->value(Qt::ImSurroundingText).toString().length() - cursorPos);
|
query->value(Qt::ImSurroundingText).toString().length() - textBeforeCursorLen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leftLength = qMin(qMax(0, textBeforeCursorLen - (initialCursorPos - leftEnd)), leftLength);
|
||||||
|
rightLength = qMin(qMax(0, textAfterCursorLen - (rightBegin - initialCursorPos)), rightLength);
|
||||||
|
|
||||||
if (leftLength == 0 && rightLength == 0)
|
if (leftLength == 0 && rightLength == 0)
|
||||||
return JNI_TRUE;
|
return JNI_TRUE;
|
||||||
|
|
||||||
QInputMethodEvent event;
|
if (leftEnd == rightBegin) {
|
||||||
event.setCommitString(QString(), -leftLength, leftLength+rightLength);
|
// We have no selection and no composing region; we can do everything using one event
|
||||||
sendInputMethodEvent(&event);
|
QInputMethodEvent event;
|
||||||
clear();
|
event.setCommitString({}, -leftLength, leftLength + rightLength);
|
||||||
|
QGuiApplication::sendEvent(m_focusObject, &event);
|
||||||
|
} else {
|
||||||
|
if (initialCursorPos != initialAnchorPos) {
|
||||||
|
QInputMethodEvent event({}, {
|
||||||
|
{ QInputMethodEvent::Selection, initialCursorPos - initialBlockPos, 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
QGuiApplication::sendEvent(m_focusObject, &event);
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentCursorPos = initialCursorPos;
|
||||||
|
|
||||||
|
if (rightLength > 0) {
|
||||||
|
QInputMethodEvent event;
|
||||||
|
event.setCommitString({}, rightBegin - currentCursorPos, rightLength);
|
||||||
|
QGuiApplication::sendEvent(m_focusObject, &event);
|
||||||
|
|
||||||
|
currentCursorPos = rightBegin;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftLength > 0) {
|
||||||
|
const int leftBegin = leftEnd - leftLength;
|
||||||
|
|
||||||
|
QInputMethodEvent event;
|
||||||
|
event.setCommitString({}, leftBegin - currentCursorPos, leftLength);
|
||||||
|
QGuiApplication::sendEvent(m_focusObject, &event);
|
||||||
|
|
||||||
|
currentCursorPos = leftBegin;
|
||||||
|
|
||||||
|
if (!m_composingText.isEmpty())
|
||||||
|
m_composingTextStart -= leftLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore cursor position or selection
|
||||||
|
if (currentCursorPos != initialCursorPos - leftLength
|
||||||
|
|| initialCursorPos != initialAnchorPos) {
|
||||||
|
// If we have deleted a newline character, we are now in a new block
|
||||||
|
const int currentBlockPos = getBlockPosition(
|
||||||
|
focusObjectInputMethodQuery(Qt::ImAbsolutePosition | Qt::ImCursorPosition));
|
||||||
|
|
||||||
|
QInputMethodEvent event({}, {
|
||||||
|
{ QInputMethodEvent::Selection, initialCursorPos - leftLength - currentBlockPos,
|
||||||
|
initialAnchorPos - initialCursorPos },
|
||||||
|
{ QInputMethodEvent::Cursor, 0, 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
QGuiApplication::sendEvent(m_focusObject, &event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return JNI_TRUE;
|
return JNI_TRUE;
|
||||||
}
|
}
|
||||||
@ -1037,16 +1115,70 @@ jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint right
|
|||||||
// Android docs say the cursor must not move
|
// Android docs say the cursor must not move
|
||||||
jboolean QAndroidInputContext::finishComposingText()
|
jboolean QAndroidInputContext::finishComposingText()
|
||||||
{
|
{
|
||||||
if (m_composingText.isEmpty())
|
BatchEditLock batchEditLock(this);
|
||||||
return JNI_TRUE; // not composing
|
|
||||||
|
if (!focusObjectStopComposing())
|
||||||
|
return JNI_FALSE;
|
||||||
|
|
||||||
|
clear();
|
||||||
|
return JNI_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QAndroidInputContext::focusObjectIsComposing() const
|
||||||
|
{
|
||||||
|
return m_composingCursor != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void QAndroidInputContext::focusObjectStartComposing()
|
||||||
|
{
|
||||||
|
if (focusObjectIsComposing() || m_composingText.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Composing strings containing newline characters are rare and may cause problems
|
||||||
|
if (m_composingText.contains(QLatin1Char('\n')))
|
||||||
|
return;
|
||||||
|
|
||||||
|
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
|
||||||
|
if (!query)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (query->value(Qt::ImCursorPosition).toInt() != query->value(Qt::ImAnchorPosition).toInt())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int absoluteCursorPos = getAbsoluteCursorPosition(query);
|
||||||
|
if (absoluteCursorPos < m_composingTextStart
|
||||||
|
|| absoluteCursorPos > m_composingTextStart + m_composingText.length())
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_composingCursor = absoluteCursorPos;
|
||||||
|
|
||||||
|
QTextCharFormat underlined;
|
||||||
|
underlined.setFontUnderline(true);
|
||||||
|
|
||||||
|
QInputMethodEvent event(m_composingText, {
|
||||||
|
{ QInputMethodEvent::Cursor, absoluteCursorPos - m_composingTextStart, 1 },
|
||||||
|
{ QInputMethodEvent::TextFormat, 0, m_composingText.length(), underlined }
|
||||||
|
});
|
||||||
|
|
||||||
|
event.setCommitString({}, m_composingTextStart - absoluteCursorPos, m_composingText.length());
|
||||||
|
|
||||||
|
QGuiApplication::sendEvent(m_focusObject, &event);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QAndroidInputContext::focusObjectStopComposing()
|
||||||
|
{
|
||||||
|
if (!focusObjectIsComposing())
|
||||||
|
return true; // not composing
|
||||||
|
|
||||||
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
|
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
|
||||||
if (query.isNull())
|
if (query.isNull())
|
||||||
return JNI_FALSE;
|
return false;
|
||||||
|
|
||||||
const int blockPos = getBlockPosition(query);
|
const int blockPos = getBlockPosition(query);
|
||||||
const int localCursorPos = m_composingCursor - blockPos;
|
const int localCursorPos = m_composingCursor - blockPos;
|
||||||
|
|
||||||
|
m_composingCursor = -1;
|
||||||
|
|
||||||
// Moving Qt's cursor to where the preedit cursor used to be
|
// Moving Qt's cursor to where the preedit cursor used to be
|
||||||
QList<QInputMethodEvent::Attribute> attributes;
|
QList<QInputMethodEvent::Attribute> attributes;
|
||||||
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, localCursorPos, 0));
|
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, localCursorPos, 0));
|
||||||
@ -1054,9 +1186,8 @@ jboolean QAndroidInputContext::finishComposingText()
|
|||||||
QInputMethodEvent event(QString(), attributes);
|
QInputMethodEvent event(QString(), attributes);
|
||||||
event.setCommitString(m_composingText);
|
event.setCommitString(m_composingText);
|
||||||
sendInputMethodEvent(&event);
|
sendInputMethodEvent(&event);
|
||||||
clear();
|
|
||||||
|
|
||||||
return JNI_TRUE;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
jint QAndroidInputContext::getCursorCapsMode(jint /*reqModes*/)
|
jint QAndroidInputContext::getCursorCapsMode(jint /*reqModes*/)
|
||||||
@ -1096,52 +1227,51 @@ const QAndroidInputContext::ExtractedText &QAndroidInputContext::getExtractedTex
|
|||||||
// updateExtractedText(View, int, ExtractedText) whenever you call
|
// updateExtractedText(View, int, ExtractedText) whenever you call
|
||||||
// updateSelection(View, int, int, int, int)." QTBUG-37980
|
// updateSelection(View, int, int, int, int)." QTBUG-37980
|
||||||
|
|
||||||
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
|
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(
|
||||||
|
Qt::ImCursorPosition | Qt::ImAbsolutePosition | Qt::ImAnchorPosition);
|
||||||
if (query.isNull())
|
if (query.isNull())
|
||||||
return m_extractedText;
|
return m_extractedText;
|
||||||
|
|
||||||
int localPos = query->value(Qt::ImCursorPosition).toInt(); //position before pre-edit text relative to the current block
|
const int cursorPos = getAbsoluteCursorPosition(query);
|
||||||
int blockPos = getBlockPosition(query);
|
const int blockPos = getBlockPosition(query);
|
||||||
QString blockText = query->value(Qt::ImSurroundingText).toString();
|
|
||||||
int composeLength = m_composingText.length();
|
|
||||||
|
|
||||||
if (composeLength > 0) {
|
|
||||||
//Qt doesn't give us the preedit text, so we have to insert it at the correct position
|
|
||||||
int localComposePos = m_composingTextStart - blockPos;
|
|
||||||
blockText = blockText.leftRef(localComposePos) + m_composingText + blockText.midRef(localComposePos);
|
|
||||||
}
|
|
||||||
|
|
||||||
int cpos = localPos + composeLength; //actual cursor pos relative to the current block
|
|
||||||
|
|
||||||
int localOffset = 0; // start of extracted text relative to the current block
|
|
||||||
if (blockPos > 0) {
|
|
||||||
QString prevBlockEnding = query->value(Qt::ImTextBeforeCursor).toString();
|
|
||||||
prevBlockEnding.chop(localPos);
|
|
||||||
if (prevBlockEnding.endsWith(QLatin1Char('\n'))) {
|
|
||||||
localOffset = -qMin(20, prevBlockEnding.length());
|
|
||||||
blockText = prevBlockEnding.right(-localOffset) + blockText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// It is documented that we should try to return hintMaxChars
|
// It is documented that we should try to return hintMaxChars
|
||||||
// characters, but that's not what the standard Android controls do, and
|
// characters, but standard Android controls always return all text, and
|
||||||
// there are input methods out there that (surprise) seem to depend on
|
// there are input methods out there that (surprise) seem to depend on
|
||||||
// what happens in reality rather than what's documented.
|
// what happens in reality rather than what's documented.
|
||||||
|
|
||||||
m_extractedText.text = blockText;
|
QVariant textBeforeCursor = QInputMethod::queryFocusObject(Qt::ImTextBeforeCursor, INT_MAX);
|
||||||
m_extractedText.startOffset = blockPos + localOffset;
|
QVariant textAfterCursor = QInputMethod::queryFocusObject(Qt::ImTextAfterCursor, INT_MAX);
|
||||||
|
if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
|
||||||
|
if (focusObjectIsComposing()) {
|
||||||
|
m_extractedText.text =
|
||||||
|
textBeforeCursor.toString() + m_composingText + textAfterCursor.toString();
|
||||||
|
} else {
|
||||||
|
m_extractedText.text = textBeforeCursor.toString() + textAfterCursor.toString();
|
||||||
|
}
|
||||||
|
|
||||||
const QString &selection = query->value(Qt::ImCurrentSelection).toString();
|
m_extractedText.startOffset = qMax(0, cursorPos - textBeforeCursor.toString().length());
|
||||||
const int selLen = selection.length();
|
} else {
|
||||||
if (selLen) {
|
m_extractedText.text = focusObjectInputMethodQuery(Qt::ImSurroundingText)
|
||||||
m_extractedText.selectionStart = query->value(Qt::ImAnchorPosition).toInt() - localOffset;
|
->value(Qt::ImSurroundingText).toString();
|
||||||
m_extractedText.selectionEnd = m_extractedText.selectionStart + selLen;
|
|
||||||
} else if (composeLength > 0) {
|
if (focusObjectIsComposing())
|
||||||
|
m_extractedText.text.insert(cursorPos - blockPos, m_composingText);
|
||||||
|
|
||||||
|
m_extractedText.startOffset = blockPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (focusObjectIsComposing()) {
|
||||||
m_extractedText.selectionStart = m_composingCursor - m_extractedText.startOffset;
|
m_extractedText.selectionStart = m_composingCursor - m_extractedText.startOffset;
|
||||||
m_extractedText.selectionEnd = m_composingCursor - m_extractedText.startOffset;
|
m_extractedText.selectionEnd = m_extractedText.selectionStart;
|
||||||
} else {
|
} else {
|
||||||
m_extractedText.selectionStart = cpos - localOffset;
|
m_extractedText.selectionStart = cursorPos - m_extractedText.startOffset;
|
||||||
m_extractedText.selectionEnd = cpos - localOffset;
|
m_extractedText.selectionEnd =
|
||||||
|
blockPos + query->value(Qt::ImAnchorPosition).toInt() - m_extractedText.startOffset;
|
||||||
|
|
||||||
|
// Some keyboards misbehave when selectionStart > selectionEnd
|
||||||
|
if (m_extractedText.selectionStart > m_extractedText.selectionEnd)
|
||||||
|
std::swap(m_extractedText.selectionStart, m_extractedText.selectionEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
return m_extractedText;
|
return m_extractedText;
|
||||||
@ -1176,10 +1306,20 @@ QString QAndroidInputContext::getTextAfterCursor(jint length, jint /*flags*/)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Controls do not report preedit text, so we have to add it
|
if (focusObjectIsComposing()) {
|
||||||
if (!m_composingText.isEmpty()) {
|
// Controls do not report preedit text, so we have to add it
|
||||||
const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
|
const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
|
||||||
text = m_composingText.midRef(cursorPosInsidePreedit) + text;
|
text = m_composingText.midRef(cursorPosInsidePreedit) + text;
|
||||||
|
} else {
|
||||||
|
// We must not return selected text if there is any
|
||||||
|
QSharedPointer<QInputMethodQueryEvent> query =
|
||||||
|
focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAnchorPosition);
|
||||||
|
if (query) {
|
||||||
|
const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
|
||||||
|
const int anchorPos = query->value(Qt::ImAnchorPosition).toInt();
|
||||||
|
if (anchorPos > cursorPos)
|
||||||
|
text.remove(0, anchorPos - cursorPos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
text.truncate(length);
|
text.truncate(length);
|
||||||
@ -1206,10 +1346,20 @@ QString QAndroidInputContext::getTextBeforeCursor(jint length, jint /*flags*/)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Controls do not report preedit text, so we have to add it
|
if (focusObjectIsComposing()) {
|
||||||
if (!m_composingText.isEmpty()) {
|
// Controls do not report preedit text, so we have to add it
|
||||||
const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
|
const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
|
||||||
text += m_composingText.leftRef(cursorPosInsidePreedit);
|
text += m_composingText.leftRef(cursorPosInsidePreedit);
|
||||||
|
} else {
|
||||||
|
// We must not return selected text if there is any
|
||||||
|
QSharedPointer<QInputMethodQueryEvent> query =
|
||||||
|
focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAnchorPosition);
|
||||||
|
if (query) {
|
||||||
|
const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
|
||||||
|
const int anchorPos = query->value(Qt::ImAnchorPosition).toInt();
|
||||||
|
if (anchorPos < cursorPos)
|
||||||
|
text.chop(cursorPos - anchorPos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text.length() > length)
|
if (text.length() > length)
|
||||||
@ -1218,11 +1368,13 @@ QString QAndroidInputContext::getTextBeforeCursor(jint length, jint /*flags*/)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Android docs say that this function should remove the current preedit text
|
Android docs say that this function should:
|
||||||
if any, and replace it with the given text. Any selected text should be
|
- remove the current composing text, if there is any
|
||||||
removed. The cursor is then moved to newCursorPosition. If > 0, this is
|
- otherwise remove currently selected text, if there is any
|
||||||
relative to the end of the text - 1; if <= 0, this is relative to the start
|
- insert new text in place of old composing text or, if there was none, at current cursor position
|
||||||
of the text.
|
- mark the inserted text as composing
|
||||||
|
- move cursor as specified by newCursorPosition: if > 0, it is relative to the end of inserted
|
||||||
|
text - 1; if <= 0, it is relative to the start of inserted text
|
||||||
*/
|
*/
|
||||||
|
|
||||||
jboolean QAndroidInputContext::setComposingText(const QString &text, jint newCursorPosition)
|
jboolean QAndroidInputContext::setComposingText(const QString &text, jint newCursorPosition)
|
||||||
@ -1231,47 +1383,110 @@ jboolean QAndroidInputContext::setComposingText(const QString &text, jint newCur
|
|||||||
if (query.isNull())
|
if (query.isNull())
|
||||||
return JNI_FALSE;
|
return JNI_FALSE;
|
||||||
|
|
||||||
const int cursorPos = getAbsoluteCursorPosition(query);
|
BatchEditLock batchEditLock(this);
|
||||||
if (newCursorPosition > 0)
|
|
||||||
newCursorPosition += text.length() - 1;
|
|
||||||
|
|
||||||
|
const int absoluteCursorPos = getAbsoluteCursorPosition(query);
|
||||||
|
int absoluteAnchorPos = getBlockPosition(query) + query->value(Qt::ImAnchorPosition).toInt();
|
||||||
|
|
||||||
|
// If we have composing region and selection (and therefore focusObjectIsComposing() == false),
|
||||||
|
// we must clear selection so that we won't delete it when we will be replacing composing text
|
||||||
|
if (!m_composingText.isEmpty() && absoluteCursorPos != absoluteAnchorPos) {
|
||||||
|
const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
|
||||||
|
QInputMethodEvent event({}, { { QInputMethodEvent::Selection, cursorPos, 0 } });
|
||||||
|
QGuiApplication::sendEvent(m_focusObject, &event);
|
||||||
|
|
||||||
|
absoluteAnchorPos = absoluteCursorPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we had no composing region, pretend that we had a zero-length composing region at current
|
||||||
|
// cursor position to simplify code. Also account for that we must delete selected text if there
|
||||||
|
// (still) is any.
|
||||||
|
const int effectiveAbsoluteCursorPos = qMin(absoluteCursorPos, absoluteAnchorPos);
|
||||||
|
if (m_composingTextStart == -1)
|
||||||
|
m_composingTextStart = effectiveAbsoluteCursorPos;
|
||||||
|
|
||||||
|
const int oldComposingTextLen = m_composingText.length();
|
||||||
m_composingText = text;
|
m_composingText = text;
|
||||||
m_composingTextStart = text.isEmpty() ? -1 : cursorPos;
|
|
||||||
m_composingCursor = cursorPos + newCursorPosition;
|
|
||||||
QList<QInputMethodEvent::Attribute> attributes;
|
|
||||||
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor,
|
|
||||||
newCursorPosition,
|
|
||||||
1));
|
|
||||||
// Show compose text underlined
|
|
||||||
QTextCharFormat underlined;
|
|
||||||
underlined.setFontUnderline(true);
|
|
||||||
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat,0, text.length(),
|
|
||||||
QVariant(underlined)));
|
|
||||||
|
|
||||||
QInputMethodEvent event(m_composingText, attributes);
|
const int newAbsoluteCursorPos =
|
||||||
sendInputMethodEvent(&event);
|
newCursorPosition <= 0
|
||||||
|
? m_composingTextStart + newCursorPosition
|
||||||
|
: m_composingTextStart + m_composingText.length() + newCursorPosition - 1;
|
||||||
|
|
||||||
QMetaObject::invokeMethod(this, "keyDown");
|
const bool focusObjectWasComposing = focusObjectIsComposing();
|
||||||
|
|
||||||
updateCursorPosition();
|
// Same checks as in focusObjectStartComposing()
|
||||||
|
if (!m_composingText.isEmpty() && !m_composingText.contains(QLatin1Char('\n'))
|
||||||
|
&& newAbsoluteCursorPos >= m_composingTextStart
|
||||||
|
&& newAbsoluteCursorPos <= m_composingTextStart + m_composingText.length())
|
||||||
|
m_composingCursor = newAbsoluteCursorPos;
|
||||||
|
else
|
||||||
|
m_composingCursor = -1;
|
||||||
|
|
||||||
|
QInputMethodEvent event;
|
||||||
|
if (focusObjectIsComposing()) {
|
||||||
|
QTextCharFormat underlined;
|
||||||
|
underlined.setFontUnderline(true);
|
||||||
|
|
||||||
|
event = QInputMethodEvent(m_composingText, {
|
||||||
|
{ QInputMethodEvent::TextFormat, 0, m_composingText.length(), underlined },
|
||||||
|
{ QInputMethodEvent::Cursor, m_composingCursor - m_composingTextStart, 1 }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (oldComposingTextLen > 0 && !focusObjectWasComposing) {
|
||||||
|
event.setCommitString({}, m_composingTextStart - effectiveAbsoluteCursorPos,
|
||||||
|
oldComposingTextLen);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
event = QInputMethodEvent({}, {});
|
||||||
|
|
||||||
|
if (focusObjectWasComposing) {
|
||||||
|
event.setCommitString(m_composingText);
|
||||||
|
} else {
|
||||||
|
event.setCommitString(m_composingText,
|
||||||
|
m_composingTextStart - effectiveAbsoluteCursorPos,
|
||||||
|
oldComposingTextLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_composingText.isEmpty())
|
||||||
|
clear();
|
||||||
|
|
||||||
|
QGuiApplication::sendEvent(m_focusObject, &event);
|
||||||
|
|
||||||
|
if (!focusObjectIsComposing() && newCursorPosition != 1) {
|
||||||
|
// Move cursor using a separate event because if we have inserted or deleted a newline
|
||||||
|
// character, then we are now inside an another block
|
||||||
|
|
||||||
|
const int newBlockPos = getBlockPosition(
|
||||||
|
focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAbsolutePosition));
|
||||||
|
|
||||||
|
event = QInputMethodEvent({}, {
|
||||||
|
{ QInputMethodEvent::Selection, newAbsoluteCursorPos - newBlockPos, 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
QGuiApplication::sendEvent(m_focusObject, &event);
|
||||||
|
}
|
||||||
|
|
||||||
|
keyDown();
|
||||||
|
|
||||||
return JNI_TRUE;
|
return JNI_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Android docs say:
|
// Android docs say:
|
||||||
// * start may be after end, same meaning as if swapped
|
// * start may be after end, same meaning as if swapped
|
||||||
// * this function should not trigger updateSelection
|
// * this function should not trigger updateSelection, but Android's native EditText does trigger it
|
||||||
// * if start == end then we should stop composing
|
// * if start == end then we should stop composing
|
||||||
jboolean QAndroidInputContext::setComposingRegion(jint start, jint end)
|
jboolean QAndroidInputContext::setComposingRegion(jint start, jint end)
|
||||||
{
|
{
|
||||||
|
BatchEditLock batchEditLock(this);
|
||||||
|
|
||||||
// Qt will not include the current preedit text in the query results, and interprets all
|
// Qt will not include the current preedit text in the query results, and interprets all
|
||||||
// parameters relative to the text excluding the preedit. The simplest solution is therefore to
|
// parameters relative to the text excluding the preedit. The simplest solution is therefore to
|
||||||
// tell Qt that we commit the text before we set the new region. This may cause a little flicker, but is
|
// tell Qt that we commit the text before we set the new region. This may cause a little flicker, but is
|
||||||
// much more robust than trying to keep the two different world views in sync
|
// much more robust than trying to keep the two different world views in sync
|
||||||
|
|
||||||
bool wasComposing = !m_composingText.isEmpty();
|
finishComposingText();
|
||||||
if (wasComposing)
|
|
||||||
finishComposingText();
|
|
||||||
|
|
||||||
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
|
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
|
||||||
if (query.isNull())
|
if (query.isNull())
|
||||||
@ -1282,54 +1497,42 @@ jboolean QAndroidInputContext::setComposingRegion(jint start, jint end)
|
|||||||
if (start > end)
|
if (start > end)
|
||||||
qSwap(start, end);
|
qSwap(start, end);
|
||||||
|
|
||||||
/*
|
|
||||||
start and end are cursor positions, not character positions,
|
|
||||||
i.e. selecting the first character is done by start == 0 and end == 1,
|
|
||||||
and start == end means no character selected
|
|
||||||
|
|
||||||
Therefore, the length of the region is end - start
|
|
||||||
*/
|
|
||||||
|
|
||||||
int length = end - start;
|
|
||||||
int localPos = query->value(Qt::ImCursorPosition).toInt();
|
|
||||||
int blockPosition = getBlockPosition(query);
|
|
||||||
int localStart = start - blockPosition; // Qt uses position inside block
|
|
||||||
int currentCursor = wasComposing ? m_composingCursor : blockPosition + localPos;
|
|
||||||
|
|
||||||
ScopedValueChangeBack<bool> svcb(m_blockUpdateSelection, true);
|
|
||||||
|
|
||||||
QString text = query->value(Qt::ImSurroundingText).toString();
|
QString text = query->value(Qt::ImSurroundingText).toString();
|
||||||
|
int textOffset = getBlockPosition(query);
|
||||||
|
|
||||||
m_composingText = text.mid(localStart, length);
|
if (start < textOffset || end > textOffset + text.length()) {
|
||||||
m_composingTextStart = start;
|
const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
|
||||||
m_composingCursor = currentCursor;
|
|
||||||
|
|
||||||
//in the Qt text controls, the preedit is defined relative to the cursor position
|
if (end - textOffset > text.length()) {
|
||||||
int relativeStart = localStart - localPos;
|
const QString after = query->value(Qt::ImTextAfterCursor).toString();
|
||||||
|
const int additionalSuffixLen = after.length() - (text.length() - cursorPos);
|
||||||
|
|
||||||
QList<QInputMethodEvent::Attribute> attributes;
|
if (additionalSuffixLen > 0)
|
||||||
|
text += after.rightRef(additionalSuffixLen);
|
||||||
|
}
|
||||||
|
|
||||||
// Show compose text underlined
|
if (start < textOffset) {
|
||||||
QTextCharFormat underlined;
|
QString before = query->value(Qt::ImTextBeforeCursor).toString();
|
||||||
underlined.setFontUnderline(true);
|
before.chop(cursorPos);
|
||||||
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat,0, length,
|
|
||||||
QVariant(underlined)));
|
|
||||||
|
|
||||||
// Keep the cursor position unchanged (don't move to end of preedit)
|
if (!before.isEmpty()) {
|
||||||
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, currentCursor - start, 1));
|
text = before + text;
|
||||||
|
textOffset -= before.length();
|
||||||
QInputMethodEvent event(m_composingText, attributes);
|
}
|
||||||
event.setCommitString(QString(), relativeStart, length);
|
}
|
||||||
sendInputMethodEvent(&event);
|
|
||||||
|
|
||||||
|
if (start < textOffset || end - textOffset > text.length()) {
|
||||||
#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
|
#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
|
||||||
QSharedPointer<QInputMethodQueryEvent> query2 = focusObjectInputMethodQuery();
|
qWarning("setComposingRegion: failed to retrieve text from composing region");
|
||||||
if (!query2.isNull()) {
|
|
||||||
qDebug() << "Setting. Prev local cpos:" << localPos << "block pos:" <<blockPosition << "comp.start:" << m_composingTextStart << "rel.start:" << relativeStart << "len:" << length << "cpos attr:" << localPos - localStart;
|
|
||||||
qDebug() << "New cursor pos" << getAbsoluteCursorPosition(query2);
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
return JNI_TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_composingText = text.mid(start - textOffset, end - start);
|
||||||
|
m_composingTextStart = start;
|
||||||
|
|
||||||
return JNI_TRUE;
|
return JNI_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1339,15 +1542,18 @@ jboolean QAndroidInputContext::setSelection(jint start, jint end)
|
|||||||
if (query.isNull())
|
if (query.isNull())
|
||||||
return JNI_FALSE;
|
return JNI_FALSE;
|
||||||
|
|
||||||
|
BatchEditLock batchEditLock(this);
|
||||||
|
|
||||||
int blockPosition = getBlockPosition(query);
|
int blockPosition = getBlockPosition(query);
|
||||||
int localCursorPos = start - blockPosition;
|
int localCursorPos = start - blockPosition;
|
||||||
|
|
||||||
QList<QInputMethodEvent::Attribute> attributes;
|
if (focusObjectIsComposing() && start == end && start >= m_composingTextStart
|
||||||
if (!m_composingText.isEmpty() && start == end) {
|
&& start <= m_composingTextStart + m_composingText.length()) {
|
||||||
// not actually changing the selection; just moving the
|
// not actually changing the selection; just moving the
|
||||||
// preedit cursor
|
// preedit cursor
|
||||||
int localOldPos = query->value(Qt::ImCursorPosition).toInt();
|
int localOldPos = query->value(Qt::ImCursorPosition).toInt();
|
||||||
int pos = localCursorPos - localOldPos;
|
int pos = localCursorPos - localOldPos;
|
||||||
|
QList<QInputMethodEvent::Attribute> attributes;
|
||||||
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, pos, 1));
|
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, pos, 1));
|
||||||
|
|
||||||
//but we have to tell Qt about the compose text all over again
|
//but we have to tell Qt about the compose text all over again
|
||||||
@ -1359,21 +1565,26 @@ jboolean QAndroidInputContext::setSelection(jint start, jint end)
|
|||||||
QVariant(underlined)));
|
QVariant(underlined)));
|
||||||
m_composingCursor = start;
|
m_composingCursor = start;
|
||||||
|
|
||||||
|
QInputMethodEvent event(m_composingText, attributes);
|
||||||
|
QGuiApplication::sendEvent(m_focusObject, &event);
|
||||||
} else {
|
} else {
|
||||||
// actually changing the selection
|
// actually changing the selection
|
||||||
|
focusObjectStopComposing();
|
||||||
|
QList<QInputMethodEvent::Attribute> attributes;
|
||||||
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection,
|
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection,
|
||||||
localCursorPos,
|
localCursorPos,
|
||||||
end - start));
|
end - start));
|
||||||
|
QInputMethodEvent event({}, attributes);
|
||||||
|
QGuiApplication::sendEvent(m_focusObject, &event);
|
||||||
}
|
}
|
||||||
QInputMethodEvent event(m_composingText, attributes);
|
|
||||||
sendInputMethodEvent(&event);
|
|
||||||
updateCursorPosition();
|
|
||||||
return JNI_TRUE;
|
return JNI_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
jboolean QAndroidInputContext::selectAll()
|
jboolean QAndroidInputContext::selectAll()
|
||||||
{
|
{
|
||||||
finishComposingText();
|
BatchEditLock batchEditLock(this);
|
||||||
|
|
||||||
|
focusObjectStopComposing();
|
||||||
m_handleMode = ShowCursor;
|
m_handleMode = ShowCursor;
|
||||||
sendShortcut(QKeySequence::SelectAll);
|
sendShortcut(QKeySequence::SelectAll);
|
||||||
return JNI_TRUE;
|
return JNI_TRUE;
|
||||||
@ -1381,7 +1592,12 @@ jboolean QAndroidInputContext::selectAll()
|
|||||||
|
|
||||||
jboolean QAndroidInputContext::cut()
|
jboolean QAndroidInputContext::cut()
|
||||||
{
|
{
|
||||||
|
BatchEditLock batchEditLock(this);
|
||||||
|
|
||||||
|
// This is probably not what native EditText would do, but normally if there is selection, then
|
||||||
|
// there will be no composing region
|
||||||
finishComposingText();
|
finishComposingText();
|
||||||
|
|
||||||
m_handleMode = ShowCursor;
|
m_handleMode = ShowCursor;
|
||||||
sendShortcut(QKeySequence::Cut);
|
sendShortcut(QKeySequence::Cut);
|
||||||
return JNI_TRUE;
|
return JNI_TRUE;
|
||||||
@ -1389,7 +1605,9 @@ jboolean QAndroidInputContext::cut()
|
|||||||
|
|
||||||
jboolean QAndroidInputContext::copy()
|
jboolean QAndroidInputContext::copy()
|
||||||
{
|
{
|
||||||
finishComposingText();
|
BatchEditLock batchEditLock(this);
|
||||||
|
|
||||||
|
focusObjectStopComposing();
|
||||||
m_handleMode = ShowCursor;
|
m_handleMode = ShowCursor;
|
||||||
sendShortcut(QKeySequence::Copy);
|
sendShortcut(QKeySequence::Copy);
|
||||||
return JNI_TRUE;
|
return JNI_TRUE;
|
||||||
@ -1403,7 +1621,11 @@ jboolean QAndroidInputContext::copyURL()
|
|||||||
|
|
||||||
jboolean QAndroidInputContext::paste()
|
jboolean QAndroidInputContext::paste()
|
||||||
{
|
{
|
||||||
|
BatchEditLock batchEditLock(this);
|
||||||
|
|
||||||
|
// TODO: This is not what native EditText does
|
||||||
finishComposingText();
|
finishComposingText();
|
||||||
|
|
||||||
m_handleMode = ShowCursor;
|
m_handleMode = ShowCursor;
|
||||||
sendShortcut(QKeySequence::Paste);
|
sendShortcut(QKeySequence::Paste);
|
||||||
return JNI_TRUE;
|
return JNI_TRUE;
|
||||||
@ -1415,8 +1637,12 @@ void QAndroidInputContext::sendShortcut(const QKeySequence &sequence)
|
|||||||
const int keys = sequence[i];
|
const int keys = sequence[i];
|
||||||
Qt::Key key = Qt::Key(keys & ~Qt::KeyboardModifierMask);
|
Qt::Key key = Qt::Key(keys & ~Qt::KeyboardModifierMask);
|
||||||
Qt::KeyboardModifiers mod = Qt::KeyboardModifiers(keys & Qt::KeyboardModifierMask);
|
Qt::KeyboardModifiers mod = Qt::KeyboardModifiers(keys & Qt::KeyboardModifierMask);
|
||||||
QGuiApplication::postEvent(m_focusObject, new QKeyEvent(QEvent::KeyPress, key, mod));
|
|
||||||
QGuiApplication::postEvent(m_focusObject, new QKeyEvent(QEvent::KeyRelease, key, mod));
|
QKeyEvent pressEvent(QEvent::KeyPress, key, mod);
|
||||||
|
QKeyEvent releaseEvent(QEvent::KeyRelease, key, mod);
|
||||||
|
|
||||||
|
QGuiApplication::sendEvent(m_focusObject, &pressEvent);
|
||||||
|
QGuiApplication::sendEvent(m_focusObject, &releaseEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,6 +151,9 @@ private slots:
|
|||||||
private:
|
private:
|
||||||
void sendInputMethodEvent(QInputMethodEvent *event);
|
void sendInputMethodEvent(QInputMethodEvent *event);
|
||||||
QSharedPointer<QInputMethodQueryEvent> focusObjectInputMethodQuery(Qt::InputMethodQueries queries = Qt::ImQueryAll);
|
QSharedPointer<QInputMethodQueryEvent> focusObjectInputMethodQuery(Qt::InputMethodQueries queries = Qt::ImQueryAll);
|
||||||
|
bool focusObjectIsComposing() const;
|
||||||
|
void focusObjectStartComposing();
|
||||||
|
bool focusObjectStopComposing();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ExtractedText m_extractedText;
|
ExtractedText m_extractedText;
|
||||||
@ -158,9 +161,8 @@ private:
|
|||||||
int m_composingTextStart;
|
int m_composingTextStart;
|
||||||
int m_composingCursor;
|
int m_composingCursor;
|
||||||
QMetaObject::Connection m_updateCursorPosConnection;
|
QMetaObject::Connection m_updateCursorPosConnection;
|
||||||
bool m_blockUpdateSelection;
|
|
||||||
HandleModes m_handleMode;
|
HandleModes m_handleMode;
|
||||||
QAtomicInt m_batchEditNestingLevel;
|
int m_batchEditNestingLevel;
|
||||||
QObject *m_focusObject;
|
QObject *m_focusObject;
|
||||||
QTimer m_hideCursorHandleTimer;
|
QTimer m_hideCursorHandleTimer;
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user