From 0c1831178540462da31fd7a4b6d2e446bc84498b Mon Sep 17 00:00:00 2001 From: Erik Kurzinger Date: Thu, 13 Jun 2019 08:15:50 -0700 Subject: Track swap interval in QXcbWindow As per GLX_EXT_swap_control, the GLX swap interval is specified on a per-drawable basis. However, QGLXContext only tracks it per-context using the m_swapInterval member. If a new drawable is made current to a context, it is still necessary to call glXSwapIntervalEXT to change the swap interval, even if it has been previously called for the same context with a different drawable. However, currently, QGLXContext::makeCurrent doesn't do this if its m_swapInterval field matches the new swap interval. This change removes m_swapInterval from QGLXContext, instead tracking it in QXcbWindow. This still avoids unnecessary calls to glXSwapIntervalEXT, while ensuring the swap interval is always set for new window drawables. Change-Id: Idc34101476c6af618059f6f3d8925dee743994a3 Reviewed-by: Giuseppe D'Angelo Reviewed-by: Laszlo Agocs --- .../platforms/xcb/gl_integrations/xcb_glx/qglxintegration.cpp | 6 +++--- src/plugins/platforms/xcb/gl_integrations/xcb_glx/qglxintegration.h | 1 - src/plugins/platforms/xcb/qxcbwindow.h | 4 ++++ 3 files changed, 7 insertions(+), 4 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/xcb/gl_integrations/xcb_glx/qglxintegration.cpp b/src/plugins/platforms/xcb/gl_integrations/xcb_glx/qglxintegration.cpp index 4adf662152..f26f698e76 100644 --- a/src/plugins/platforms/xcb/gl_integrations/xcb_glx/qglxintegration.cpp +++ b/src/plugins/platforms/xcb/gl_integrations/xcb_glx/qglxintegration.cpp @@ -204,7 +204,6 @@ QGLXContext::QGLXContext(QXcbScreen *screen, const QSurfaceFormat &format, QPlat , m_shareContext(0) , m_format(format) , m_isPBufferCurrent(false) - , m_swapInterval(-1) , m_ownsContext(nativeHandle.isNull()) , m_getGraphicsResetStatus(0) , m_lost(false) @@ -567,9 +566,9 @@ bool QGLXContext::makeCurrent(QPlatformSurface *surface) if (success && surfaceClass == QSurface::Window) { int interval = surface->format().swapInterval(); + QXcbWindow *window = static_cast(surface); QXcbScreen *screen = screenForPlatformSurface(surface); - if (interval >= 0 && m_swapInterval != interval && screen) { - m_swapInterval = interval; + if (interval >= 0 && interval != window->swapInterval() && screen) { typedef void (*qt_glXSwapIntervalEXT)(Display *, GLXDrawable, int); typedef void (*qt_glXSwapIntervalMESA)(unsigned int); static qt_glXSwapIntervalEXT glXSwapIntervalEXT = 0; @@ -588,6 +587,7 @@ bool QGLXContext::makeCurrent(QPlatformSurface *surface) glXSwapIntervalEXT(m_display, glxDrawable, interval); else if (glXSwapIntervalMESA) glXSwapIntervalMESA(interval); + window->setSwapInterval(interval); } } diff --git a/src/plugins/platforms/xcb/gl_integrations/xcb_glx/qglxintegration.h b/src/plugins/platforms/xcb/gl_integrations/xcb_glx/qglxintegration.h index be9d3f5dcb..2a88fd6e59 100644 --- a/src/plugins/platforms/xcb/gl_integrations/xcb_glx/qglxintegration.h +++ b/src/plugins/platforms/xcb/gl_integrations/xcb_glx/qglxintegration.h @@ -87,7 +87,6 @@ private: GLXContext m_shareContext; QSurfaceFormat m_format; bool m_isPBufferCurrent; - int m_swapInterval; bool m_ownsContext; GLenum (APIENTRY * m_getGraphicsResetStatus)(); bool m_lost; diff --git a/src/plugins/platforms/xcb/qxcbwindow.h b/src/plugins/platforms/xcb/qxcbwindow.h index f98cd8a74d..8258cc2dfa 100644 --- a/src/plugins/platforms/xcb/qxcbwindow.h +++ b/src/plugins/platforms/xcb/qxcbwindow.h @@ -184,6 +184,9 @@ public: static void setWindowTitle(const QXcbConnection *conn, xcb_window_t window, const QString &title); static QString windowTitle(const QXcbConnection *conn, xcb_window_t window); + int swapInterval() const { return m_swapInterval; } + void setSwapInterval(int swapInterval) { m_swapInterval = swapInterval; } + public Q_SLOTS: void updateSyncRequestCounter(); @@ -276,6 +279,7 @@ protected: SyncState m_syncState = NoSyncNeeded; QXcbSyncWindowRequest *m_pendingSyncRequest = nullptr; + int m_swapInterval = -1; }; class QXcbForeignWindow : public QXcbWindow -- cgit v1.2.3 From 710435ee81c4cf48d66f07eff3cbad3eaef80ab8 Mon Sep 17 00:00:00 2001 From: Vova Mshanetskiy Date: Mon, 6 May 2019 15:00:55 +0300 Subject: QAndroidInputContext: Fix unneeded preedit commits when dragging handles If the cursor handle was dragged by only a few pixels, position of the cursor did not actually change, but finishComposingText() was called anyway. So the keyboard was thinking that nothing changed and a word is still being composed, but the app was thinking that there is no preedit string. This was resulting in invalid handling of following key presses. This commit essentially inlines QPlatformInputContext::setSelectionOnFocusObject() into QAndroidInputContext::handleLocationChanged(). This allows us to call finishComposingText() and to send a QInputMethodEvent only when position of the cursur actually changes. This also allows us to add a QInputMethodEvent::Cursor attribute into the event for consistency with QAndroidInputContext::longPress(). Change-Id: I2fc82f138f717991f34024cdf521236845dc0adf Reviewed-by: BogDan Vatra --- .../platforms/android/qandroidinputcontext.cpp | 63 ++++++++++++++++++---- 1 file changed, 52 insertions(+), 11 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/android/qandroidinputcontext.cpp b/src/plugins/platforms/android/qandroidinputcontext.cpp index db40c30d7d..4c0b3315be 100644 --- a/src/plugins/platforms/android/qandroidinputcontext.cpp +++ b/src/plugins/platforms/android/qandroidinputcontext.cpp @@ -671,8 +671,6 @@ void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y) return; } - finishComposingText(); - auto im = qGuiApp->inputMethod(); auto leftRect = im->cursorRectangle(); // The handle is down of the cursor, but we want the position in the middle. @@ -682,12 +680,9 @@ void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y) : QHighDpiScaling::factor(QtAndroid::androidPlatformIntegration()->screen()); QPointF point(x / pixelDensity, y / pixelDensity); point.setY(point.y() - leftRect.width() / 2); - if (handleId == 1) { - setSelectionOnFocusObject(point, point); - return; - } - QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition | Qt::ImCurrentSelection); + QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition + | Qt::ImAbsolutePosition | Qt::ImCurrentSelection); QCoreApplication::sendEvent(m_focusObject, &query); int cpos = query.value(Qt::ImCursorPosition).toInt(); int anchor = query.value(Qt::ImAnchorPosition).toInt(); @@ -729,16 +724,62 @@ void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y) }; if (handleId == 2) { - QPointF rightPoint(rightRect.center()); if ((!rtl && !checkLeftHandle(point)) || (rtl && !checkRtlRightHandle(point))) return; - setSelectionOnFocusObject(point, rightPoint); } else if (handleId == 3) { - QPointF leftPoint(leftRect.center()); if ((!rtl && !checkRightHandle(point)) || (rtl && !checkRtlLeftHandle(point))) return; - setSelectionOnFocusObject(leftPoint, point); } + + const QPointF pointLocal = im->inputItemTransform().inverted().map(point); + bool ok; + const int handlePos = + QInputMethod::queryFocusObject(Qt::ImCursorPosition, pointLocal).toInt(&ok); + if (!ok) + return; + + int newCpos = cpos; + int newAnchor = anchor; + if (newAnchor > newCpos) + std::swap(newAnchor, newCpos); + + if (handleId == 1) { + newCpos = handlePos; + newAnchor = handlePos; + } else if (handleId == 2) { + newAnchor = handlePos; + } else if (handleId == 3) { + newCpos = handlePos; + } + + // Check if handle has been dragged far enough + if (m_composingText.isEmpty() && newCpos == cpos && newAnchor == anchor) + return; + + /* + If there is composing text, we have to compare newCpos with m_composingCursor instead of cpos. + And since there is nothing to compare with newAnchor, we perform the check only when user + drags the cursor handle. + */ + if (!m_composingText.isEmpty() && handleId == 1) { + int absoluteCpos = query.value(Qt::ImAbsolutePosition).toInt(&ok); + if (!ok) + absoluteCpos = cpos; + const int blockPos = absoluteCpos - cpos; + + if (blockPos + newCpos == m_composingCursor) + return; + } + + finishComposingText(); + + QList attributes; + attributes.append({ QInputMethodEvent::Selection, newAnchor, newCpos - newAnchor }); + if (newCpos != newAnchor) + attributes.append({ QInputMethodEvent::Cursor, 0, 0 }); + + QInputMethodEvent event(QString(), attributes); + QGuiApplication::sendEvent(m_focusObject, &event); } void QAndroidInputContext::touchDown(int x, int y) -- cgit v1.2.3 From e5f2be256fb3bb07a98693cb90a4d7e8bc63c508 Mon Sep 17 00:00:00 2001 From: Vova Mshanetskiy Date: Mon, 6 May 2019 19:10:09 +0300 Subject: QAndroidInputContext: Don't allow clearing selection by dragging handles Android's native text editing controls do not allow user to clear selection by dragging selection handles. Qt apps should behave in the same way. Change-Id: I9a7c3a2aafa484eed8ff2bbd46dd48c705195291 Reviewed-by: BogDan Vatra --- .../platforms/android/qandroidinputcontext.cpp | 66 +++++++++------------- 1 file changed, 27 insertions(+), 39 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/android/qandroidinputcontext.cpp b/src/plugins/platforms/android/qandroidinputcontext.cpp index 4c0b3315be..4d9af18053 100644 --- a/src/plugins/platforms/android/qandroidinputcontext.cpp +++ b/src/plugins/platforms/android/qandroidinputcontext.cpp @@ -686,49 +686,15 @@ void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y) QCoreApplication::sendEvent(m_focusObject, &query); int cpos = query.value(Qt::ImCursorPosition).toInt(); int anchor = query.value(Qt::ImAnchorPosition).toInt(); - bool rtl = query.value(Qt::ImCurrentSelection).toString().isRightToLeft(); auto rightRect = im->anchorRectangle(); if (cpos > anchor) std::swap(leftRect, rightRect); - auto checkLeftHandle = [&rightRect](QPointF &handlePos) { - if (handlePos.y() > rightRect.center().y()) - handlePos.setY(rightRect.center().y()); // adjust Y handle pos - if (handlePos.y() >= rightRect.y() && handlePos.y() <= rightRect.bottom() && handlePos.x() >= rightRect.x()) - return false; // same line and wrong X pos ? - return true; - }; - - auto checkRtlRightHandle = [&rightRect](QPointF &handlePos) { - if (handlePos.y() > rightRect.center().y()) - handlePos.setY(rightRect.center().y()); // adjust Y handle pos - if (handlePos.y() >= rightRect.y() && handlePos.y() <= rightRect.bottom() && rightRect.x() >= handlePos.x()) - return false; // same line and wrong X pos ? - return true; - }; - - auto checkRightHandle = [&leftRect](QPointF &handlePos) { - if (handlePos.y() < leftRect.center().y()) - handlePos.setY(leftRect.center().y()); // adjust Y handle pos - if (handlePos.y() >= leftRect.y() && handlePos.y() <= leftRect.bottom() && leftRect.x() >= handlePos.x()) - return false; // same line and wrong X pos ? - return true; - }; - - auto checkRtlLeftHandle = [&leftRect](QPointF &handlePos) { - if (handlePos.y() < leftRect.center().y()) - handlePos.setY(leftRect.center().y()); // adjust Y handle pos - if (handlePos.y() >= leftRect.y() && handlePos.y() <= leftRect.bottom() && handlePos.x() >= leftRect.x()) - return false; // same line and wrong X pos ? - return true; - }; - - if (handleId == 2) { - if ((!rtl && !checkLeftHandle(point)) || (rtl && !checkRtlRightHandle(point))) - return; - } else if (handleId == 3) { - if ((!rtl && !checkRightHandle(point)) || (rtl && !checkRtlLeftHandle(point))) - return; + // Do not allow dragging left handle below right handle, or right handle above left handle + if (handleId == 2 && point.y() > rightRect.center().y()) { + point.setY(rightRect.center().y()); + } else if (handleId == 3 && point.y() < leftRect.center().y()) { + point.setY(leftRect.center().y()); } const QPointF pointLocal = im->inputItemTransform().inverted().map(point); @@ -752,6 +718,28 @@ void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y) newCpos = handlePos; } + /* + Do not allow clearing selection by dragging selection handles and do not allow swapping + selection handles for consistency with Android's native text editing controls. Ensure that at + least one symbol remains selected. + */ + if ((handleId == 2 || handleId == 3) && newCpos <= newAnchor) { + QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, + query.value(Qt::ImCurrentSelection).toString()); + + const int oldSelectionStartPos = qMin(cpos, anchor); + + if (handleId == 2) { + finder.toEnd(); + finder.toPreviousBoundary(); + newAnchor = finder.position() + oldSelectionStartPos; + } else { + finder.toStart(); + finder.toNextBoundary(); + newCpos = finder.position() + oldSelectionStartPos; + } + } + // Check if handle has been dragged far enough if (m_composingText.isEmpty() && newCpos == cpos && newAnchor == anchor) return; -- cgit v1.2.3 From 1ade5ea41ab80e49e92fe46e0c44f76ee2e5e7fb Mon Sep 17 00:00:00 2001 From: Vova Mshanetskiy Date: Fri, 31 May 2019 16:25:14 +0300 Subject: 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 --- .../platforms/android/qandroidinputcontext.cpp | 626 ++++++++++++++------- .../platforms/android/qandroidinputcontext.h | 6 +- 2 files changed, 430 insertions(+), 202 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/android/qandroidinputcontext.cpp b/src/plugins/platforms/android/qandroidinputcontext.cpp index 4d9af18053..fa07af8c46 100644 --- a/src/plugins/platforms/android/qandroidinputcontext.cpp +++ b/src/plugins/platforms/android/qandroidinputcontext.cpp @@ -64,29 +64,33 @@ QT_BEGIN_NAMESPACE -template -class ScopedValueChangeBack +namespace { + +class BatchEditLock { public: - ScopedValueChangeBack(T &variable, T newValue) - : m_oldValue(variable), - m_variable(variable) - { - m_variable = newValue; - } - inline void setOldValue() + + explicit BatchEditLock(QAndroidInputContext *context) + : m_context(context) { - m_variable = m_oldValue; + m_context->beginBatchEdit(); } - ~ScopedValueChangeBack() + + ~BatchEditLock() { - setOldValue(); + m_context->endBatchEdit(); } + + BatchEditLock(const BatchEditLock &) = delete; + BatchEditLock &operator=(const BatchEditLock &) = delete; + private: - T m_oldValue; - T &m_variable; + + QAndroidInputContext *m_context; }; +} // namespace anonymous + static QAndroidInputContext *m_androidInputContext = 0; static char const *const QtNativeInputConnectionClassName = "org/qtproject/qt5/android/QtNativeInputConnection"; static char const *const QtExtractedTextClassName = "org/qtproject/qt5/android/QtExtractedText"; @@ -423,8 +427,12 @@ static QRect inputItemRectangle() } QAndroidInputContext::QAndroidInputContext() - : QPlatformInputContext(), m_composingTextStart(-1), m_blockUpdateSelection(false), - m_handleMode(Hidden), m_batchEditNestingLevel(0), m_focusObject(0) + : QPlatformInputContext() + , m_composingTextStart(-1) + , m_composingCursor(-1) + , m_handleMode(Hidden) + , m_batchEditNestingLevel(0) + , m_focusObject(0) { jclass clazz = QJNIEnvironmentPrivate::findClass(QtNativeInputConnectionClassName); if (Q_UNLIKELY(!clazz)) { @@ -565,13 +573,13 @@ void QAndroidInputContext::reset() void QAndroidInputContext::commit() { - finishComposingText(); + focusObjectStopComposing(); } void QAndroidInputContext::updateCursorPosition() { QSharedPointer query = focusObjectInputMethodQuery(); - if (!query.isNull() && !m_blockUpdateSelection && !m_batchEditNestingLevel) { + if (!query.isNull() && m_batchEditNestingLevel == 0) { const int cursorPos = getAbsoluteCursorPosition(query); const int composeLength = m_composingText.length(); @@ -579,24 +587,29 @@ void QAndroidInputContext::updateCursorPosition() if (m_composingText.isEmpty() != (m_composingTextStart == -1)) qWarning() << "Input method out of sync" << m_composingText << m_composingTextStart; - int realCursorPosition = cursorPos; - int realAnchorPosition = cursorPos; + int realSelectionStart = cursorPos; + int realSelectionEnd = cursorPos; int cpos = query->value(Qt::ImCursorPosition).toInt(); int anchor = query->value(Qt::ImAnchorPosition).toInt(); if (cpos != anchor) { if (!m_composingText.isEmpty()) { qWarning("Selecting text while preediting may give unpredictable results."); - finishComposingText(); + focusObjectStopComposing(); } int blockPos = getBlockPosition(query); - realCursorPosition = blockPos + cpos; - realAnchorPosition = blockPos + anchor; + realSelectionStart = blockPos + cpos; + 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 - if (!m_composingText.isEmpty()) - realCursorPosition = realAnchorPosition = m_composingCursor; - QtAndroidInput::updateSelection(realCursorPosition, realAnchorPosition, + if (focusObjectIsComposing()) + realSelectionStart = realSelectionEnd = m_composingCursor; + + // 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 } } @@ -666,7 +679,7 @@ void QAndroidInputContext::updateSelectionHandles() */ void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y) { - if (m_batchEditNestingLevel.load() || m_blockUpdateSelection) { + if (m_batchEditNestingLevel != 0) { qWarning() << "QAndroidInputContext::handleLocationChanged returned"; return; } @@ -741,15 +754,15 @@ void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y) } // Check if handle has been dragged far enough - if (m_composingText.isEmpty() && newCpos == cpos && newAnchor == anchor) + if (!focusObjectIsComposing() && newCpos == cpos && newAnchor == anchor) return; /* - If there is composing text, we have to compare newCpos with m_composingCursor instead of cpos. - And since there is nothing to compare with newAnchor, we perform the check only when user - drags the cursor handle. + If the editor is currently in composing state, we have to compare newCpos with + m_composingCursor instead of cpos. And since there is nothing to compare with newAnchor, we + 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); if (!ok) absoluteCpos = cpos; @@ -759,7 +772,9 @@ void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y) return; } - finishComposingText(); + BatchEditLock batchEditLock(this); + + focusObjectStopComposing(); QList attributes; attributes.append({ QInputMethodEvent::Selection, newAnchor, newCpos - newAnchor }); @@ -777,7 +792,7 @@ void QAndroidInputContext::touchDown(int x, int y) m_handleMode = ShowCursor; // The VK will appear in a moment, stop the timer m_hideCursorHandleTimer.stop(); - finishComposingText(); + focusObjectStopComposing(); updateSelectionHandles(); } } @@ -789,13 +804,19 @@ void QAndroidInputContext::longPress(int x, int y) return; if (m_focusObject && inputItemRectangle().contains(x, y)) { - finishComposingText(); + BatchEditLock batchEditLock(this); + + focusObjectStopComposing(); // Release left button, otherwise the following events will cancel the menu popup QtAndroidInput::releaseMouse(x, y); - handleLocationChanged(1, x, y); - ScopedValueChangeBack svcb(m_blockUpdateSelection, true); + const double pixelDensity = + 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); QCoreApplication::sendEvent(m_focusObject, &query); @@ -934,6 +955,7 @@ void QAndroidInputContext::clear() { m_composingText.clear(); m_composingTextStart = -1; + m_composingCursor = -1; m_extractedText.clear(); } @@ -941,9 +963,8 @@ void QAndroidInputContext::clear() void QAndroidInputContext::setFocusObject(QObject *object) { if (object != m_focusObject) { + focusObjectStopComposing(); m_focusObject = object; - if (!m_composingText.isEmpty()) - finishComposingText(); reset(); } QPlatformInputContext::setFocusObject(object); @@ -958,78 +979,135 @@ jboolean QAndroidInputContext::beginBatchEdit() jboolean QAndroidInputContext::endBatchEdit() { - if (--m_batchEditNestingLevel == 0 && !m_blockUpdateSelection) //ending batch edit mode + if (--m_batchEditNestingLevel == 0) { //ending batch edit mode + focusObjectStartComposing(); updateCursorPosition(); + } return JNI_TRUE; } /* - Android docs say: If composing, replace compose text with \a text. - Otherwise insert \a text at current cursor position. - - 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. + Android docs say: This behaves like calling setComposingText(text, newCursorPosition) then + finishComposingText(). */ jboolean QAndroidInputContext::commitText(const QString &text, jint newCursorPosition) { - ScopedValueChangeBack svcb(m_blockUpdateSelection, true); - QInputMethodEvent event; - 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 query = focusObjectInputMethodQuery(); - if (!query.isNull()) { - QList 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; + BatchEditLock batchEditLock(this); + return setComposingText(text, newCursorPosition) && finishComposingText(); } jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint rightLength) { + BatchEditLock batchEditLock(this); + + focusObjectStopComposing(); + QSharedPointer query = focusObjectInputMethodQuery(); if (query.isNull()) return JNI_TRUE; - m_composingText.clear(); - m_composingTextStart = -1; - if (leftLength < 0) { rightLength += -leftLength; 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 textAfterCursor = query->value(Qt::ImTextAfterCursor); if (textBeforeCursor.isValid() && textAfterCursor.isValid()) { - leftLength = qMin(leftLength, textBeforeCursor.toString().length()); - rightLength = qMin(rightLength, textAfterCursor.toString().length()); + textBeforeCursorLen = textBeforeCursor.toString().length(); + textAfterCursorLen = textAfterCursor.toString().length(); } else { - int cursorPos = query->value(Qt::ImCursorPosition).toInt(); - leftLength = qMin(leftLength, cursorPos); - rightLength = qMin(rightLength, query->value(Qt::ImSurroundingText).toString().length() - cursorPos); + textBeforeCursorLen = initialCursorPos - initialBlockPos; + textAfterCursorLen = + 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) return JNI_TRUE; - QInputMethodEvent event; - event.setCommitString(QString(), -leftLength, leftLength+rightLength); - sendInputMethodEvent(&event); - clear(); + if (leftEnd == rightBegin) { + // We have no selection and no composing region; we can do everything using one event + QInputMethodEvent event; + 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; } @@ -1037,16 +1115,70 @@ jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint right // Android docs say the cursor must not move jboolean QAndroidInputContext::finishComposingText() { - if (m_composingText.isEmpty()) - return JNI_TRUE; // not composing + BatchEditLock batchEditLock(this); + + 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 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 query = focusObjectInputMethodQuery(); if (query.isNull()) - return JNI_FALSE; + return false; const int blockPos = getBlockPosition(query); const int localCursorPos = m_composingCursor - blockPos; + m_composingCursor = -1; + // Moving Qt's cursor to where the preedit cursor used to be QList attributes; attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, localCursorPos, 0)); @@ -1054,9 +1186,8 @@ jboolean QAndroidInputContext::finishComposingText() QInputMethodEvent event(QString(), attributes); event.setCommitString(m_composingText); sendInputMethodEvent(&event); - clear(); - return JNI_TRUE; + return true; } jint QAndroidInputContext::getCursorCapsMode(jint /*reqModes*/) @@ -1096,52 +1227,51 @@ const QAndroidInputContext::ExtractedText &QAndroidInputContext::getExtractedTex // updateExtractedText(View, int, ExtractedText) whenever you call // updateSelection(View, int, int, int, int)." QTBUG-37980 - QSharedPointer query = focusObjectInputMethodQuery(); + QSharedPointer query = focusObjectInputMethodQuery( + Qt::ImCursorPosition | Qt::ImAbsolutePosition | Qt::ImAnchorPosition); if (query.isNull()) return m_extractedText; - int localPos = query->value(Qt::ImCursorPosition).toInt(); //position before pre-edit text relative to the current block - 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; - } - } + const int cursorPos = getAbsoluteCursorPosition(query); + const int blockPos = getBlockPosition(query); // 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 // what happens in reality rather than what's documented. - m_extractedText.text = blockText; - m_extractedText.startOffset = blockPos + localOffset; + QVariant textBeforeCursor = QInputMethod::queryFocusObject(Qt::ImTextBeforeCursor, INT_MAX); + 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(); - const int selLen = selection.length(); - if (selLen) { - m_extractedText.selectionStart = query->value(Qt::ImAnchorPosition).toInt() - localOffset; - m_extractedText.selectionEnd = m_extractedText.selectionStart + selLen; - } else if (composeLength > 0) { + m_extractedText.startOffset = qMax(0, cursorPos - textBeforeCursor.toString().length()); + } else { + m_extractedText.text = focusObjectInputMethodQuery(Qt::ImSurroundingText) + ->value(Qt::ImSurroundingText).toString(); + + 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.selectionEnd = m_composingCursor - m_extractedText.startOffset; - } else { - m_extractedText.selectionStart = cpos - localOffset; - m_extractedText.selectionEnd = cpos - localOffset; + m_extractedText.selectionEnd = m_extractedText.selectionStart; + } else { + m_extractedText.selectionStart = cursorPos - m_extractedText.startOffset; + 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; @@ -1176,10 +1306,20 @@ QString QAndroidInputContext::getTextAfterCursor(jint length, jint /*flags*/) } } - // Controls do not report preedit text, so we have to add it - if (!m_composingText.isEmpty()) { + if (focusObjectIsComposing()) { + // Controls do not report preedit text, so we have to add it const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart; text = m_composingText.midRef(cursorPosInsidePreedit) + text; + } else { + // We must not return selected text if there is any + QSharedPointer 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); @@ -1206,10 +1346,20 @@ QString QAndroidInputContext::getTextBeforeCursor(jint length, jint /*flags*/) } } - // Controls do not report preedit text, so we have to add it - if (!m_composingText.isEmpty()) { + if (focusObjectIsComposing()) { + // Controls do not report preedit text, so we have to add it const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart; text += m_composingText.leftRef(cursorPosInsidePreedit); + } else { + // We must not return selected text if there is any + QSharedPointer 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) @@ -1218,11 +1368,13 @@ QString QAndroidInputContext::getTextBeforeCursor(jint length, jint /*flags*/) } /* - Android docs say that this function should remove the current preedit text - if any, and replace it with the given text. Any selected text should be - removed. The cursor is then 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. + Android docs say that this function should: + - remove the current composing text, if there is any + - otherwise remove currently selected text, if there is any + - insert new text in place of old composing text or, if there was none, at current cursor position + - 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) @@ -1231,47 +1383,110 @@ jboolean QAndroidInputContext::setComposingText(const QString &text, jint newCur if (query.isNull()) return JNI_FALSE; - const int cursorPos = getAbsoluteCursorPosition(query); - if (newCursorPosition > 0) - newCursorPosition += text.length() - 1; + BatchEditLock batchEditLock(this); + 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_composingTextStart = text.isEmpty() ? -1 : cursorPos; - m_composingCursor = cursorPos + newCursorPosition; - QList 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); - sendInputMethodEvent(&event); + const int newAbsoluteCursorPos = + 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; } // Android docs say: // * 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 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 // 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 // much more robust than trying to keep the two different world views in sync - bool wasComposing = !m_composingText.isEmpty(); - if (wasComposing) - finishComposingText(); + finishComposingText(); QSharedPointer query = focusObjectInputMethodQuery(); if (query.isNull()) @@ -1282,54 +1497,42 @@ jboolean QAndroidInputContext::setComposingRegion(jint start, jint end) if (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 svcb(m_blockUpdateSelection, true); - QString text = query->value(Qt::ImSurroundingText).toString(); + int textOffset = getBlockPosition(query); - m_composingText = text.mid(localStart, length); - m_composingTextStart = start; - m_composingCursor = currentCursor; - - //in the Qt text controls, the preedit is defined relative to the cursor position - int relativeStart = localStart - localPos; + if (start < textOffset || end > textOffset + text.length()) { + const int cursorPos = query->value(Qt::ImCursorPosition).toInt(); - QList attributes; + if (end - textOffset > text.length()) { + const QString after = query->value(Qt::ImTextAfterCursor).toString(); + const int additionalSuffixLen = after.length() - (text.length() - cursorPos); - // Show compose text underlined - QTextCharFormat underlined; - underlined.setFontUnderline(true); - attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat,0, length, - QVariant(underlined))); + if (additionalSuffixLen > 0) + text += after.rightRef(additionalSuffixLen); + } - // Keep the cursor position unchanged (don't move to end of preedit) - attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, currentCursor - start, 1)); + if (start < textOffset) { + QString before = query->value(Qt::ImTextBeforeCursor).toString(); + before.chop(cursorPos); - QInputMethodEvent event(m_composingText, attributes); - event.setCommitString(QString(), relativeStart, length); - sendInputMethodEvent(&event); + if (!before.isEmpty()) { + text = before + text; + textOffset -= before.length(); + } + } + if (start < textOffset || end - textOffset > text.length()) { #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - QSharedPointer query2 = focusObjectInputMethodQuery(); - if (!query2.isNull()) { - qDebug() << "Setting. Prev local cpos:" << localPos << "block pos:" < attributes; - if (!m_composingText.isEmpty() && start == end) { + if (focusObjectIsComposing() && start == end && start >= m_composingTextStart + && start <= m_composingTextStart + m_composingText.length()) { // not actually changing the selection; just moving the // preedit cursor int localOldPos = query->value(Qt::ImCursorPosition).toInt(); int pos = localCursorPos - localOldPos; + QList attributes; attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, pos, 1)); //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))); m_composingCursor = start; + QInputMethodEvent event(m_composingText, attributes); + QGuiApplication::sendEvent(m_focusObject, &event); } else { // actually changing the selection + focusObjectStopComposing(); + QList attributes; attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, localCursorPos, end - start)); + QInputMethodEvent event({}, attributes); + QGuiApplication::sendEvent(m_focusObject, &event); } - QInputMethodEvent event(m_composingText, attributes); - sendInputMethodEvent(&event); - updateCursorPosition(); return JNI_TRUE; } jboolean QAndroidInputContext::selectAll() { - finishComposingText(); + BatchEditLock batchEditLock(this); + + focusObjectStopComposing(); m_handleMode = ShowCursor; sendShortcut(QKeySequence::SelectAll); return JNI_TRUE; @@ -1381,7 +1592,12 @@ jboolean QAndroidInputContext::selectAll() 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(); + m_handleMode = ShowCursor; sendShortcut(QKeySequence::Cut); return JNI_TRUE; @@ -1389,7 +1605,9 @@ jboolean QAndroidInputContext::cut() jboolean QAndroidInputContext::copy() { - finishComposingText(); + BatchEditLock batchEditLock(this); + + focusObjectStopComposing(); m_handleMode = ShowCursor; sendShortcut(QKeySequence::Copy); return JNI_TRUE; @@ -1403,7 +1621,11 @@ jboolean QAndroidInputContext::copyURL() jboolean QAndroidInputContext::paste() { + BatchEditLock batchEditLock(this); + + // TODO: This is not what native EditText does finishComposingText(); + m_handleMode = ShowCursor; sendShortcut(QKeySequence::Paste); return JNI_TRUE; @@ -1415,8 +1637,12 @@ void QAndroidInputContext::sendShortcut(const QKeySequence &sequence) const int keys = sequence[i]; Qt::Key key = Qt::Key(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); } } diff --git a/src/plugins/platforms/android/qandroidinputcontext.h b/src/plugins/platforms/android/qandroidinputcontext.h index bd3edb30f0..e9bfb98e66 100644 --- a/src/plugins/platforms/android/qandroidinputcontext.h +++ b/src/plugins/platforms/android/qandroidinputcontext.h @@ -151,6 +151,9 @@ private slots: private: void sendInputMethodEvent(QInputMethodEvent *event); QSharedPointer focusObjectInputMethodQuery(Qt::InputMethodQueries queries = Qt::ImQueryAll); + bool focusObjectIsComposing() const; + void focusObjectStartComposing(); + bool focusObjectStopComposing(); private: ExtractedText m_extractedText; @@ -158,9 +161,8 @@ private: int m_composingTextStart; int m_composingCursor; QMetaObject::Connection m_updateCursorPosConnection; - bool m_blockUpdateSelection; HandleModes m_handleMode; - QAtomicInt m_batchEditNestingLevel; + int m_batchEditNestingLevel; QObject *m_focusObject; QTimer m_hideCursorHandleTimer; }; -- cgit v1.2.3 From 9b6928b7cc6f12638ae625c67ecf437cfc694498 Mon Sep 17 00:00:00 2001 From: Val Doroshchuk Date: Fri, 31 May 2019 14:15:53 +0200 Subject: Fix crash when app is going to shutdown but conf manager is requested MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the app is finished and going to shutdown, qNetworkConfigurationManagerPrivate() returns nullptr. Change-Id: I01915021d8698802b3a1d0dee43203cd3d4aba74 Task-number: QTBUG-76090 Reviewed-by: Mårten Nordheim --- src/plugins/bearer/qnetworksession_impl.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/bearer/qnetworksession_impl.cpp b/src/plugins/bearer/qnetworksession_impl.cpp index 847479047f..903525a204 100644 --- a/src/plugins/bearer/qnetworksession_impl.cpp +++ b/src/plugins/bearer/qnetworksession_impl.cpp @@ -56,12 +56,13 @@ QT_BEGIN_NAMESPACE static QBearerEngineImpl *getEngineFromId(const QString &id) { QNetworkConfigurationManagerPrivate *priv = qNetworkConfigurationManagerPrivate(); - - const auto engines = priv->engines(); - for (QBearerEngine *engine : engines) { - QBearerEngineImpl *engineImpl = qobject_cast(engine); - if (engineImpl && engineImpl->hasIdentifier(id)) - return engineImpl; + if (priv) { + const auto engines = priv->engines(); + for (QBearerEngine *engine : engines) { + QBearerEngineImpl *engineImpl = qobject_cast(engine); + if (engineImpl && engineImpl->hasIdentifier(id)) + return engineImpl; + } } return 0; -- cgit v1.2.3 From 09a2a9bc4a4a5c9ef7849d3f30243cd8e2d389d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Johan=20S=C3=B8rvig?= Date: Mon, 18 Mar 2019 13:37:35 +0100 Subject: Cocoa: always send queued user input events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User input events will be queued if processEvents() is called with the ExcludeUserInputEvents flag. User code then expect that the queued events will be sent when the corresponding exec() call returns. We were sending queued user input event at the beginning of processEvents(). However, the cocoa event dispatcher also has a mode where it makes a blocking call to [NSApp run], in which case processEvents() never returns during event processing. This means we don’t get to call the queued-event-sending code. Factor out the queued-event-sending code to a new sendQueuedUserInputEvents() function. Call it from postedEventsSourceCallback() to make sure the queue is emptied after the ExcludeUserInputEvents processEvents() call is done. Task-number: QTBUG-69687 Change-Id: I4ff554ef4d39a69356736c33a650886b56bfdb4c Reviewed-by: Timur Pocheptsov Reviewed-by: Richard Moe Gustavsen --- .../platforms/cocoa/qcocoaeventdispatcher.h | 1 + .../platforms/cocoa/qcocoaeventdispatcher.mm | 31 +++++++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h index 9771cd0289..69587a24be 100644 --- a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h +++ b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h @@ -191,6 +191,7 @@ public: static void waitingObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info); static void firstLoopEntry(CFRunLoopObserverRef ref, CFRunLoopActivity activity, void *info); + bool sendQueuedUserInputEvents(); void processPostedEvents(); }; diff --git a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm index 84ffadea83..d3bb0711f0 100644 --- a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm +++ b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm @@ -377,16 +377,9 @@ bool QCocoaEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags) NSEvent* event = nil; // First, send all previously excluded input events, if any: - if (!excludeUserEvents) { - while (!d->queuedUserInputEvents.isEmpty()) { - event = static_cast(d->queuedUserInputEvents.takeFirst()); - if (!filterNativeEvent("NSEvent", event, nullptr)) { - [NSApp sendEvent:event]; - retVal = true; - } - [event release]; - } - } + if (d->sendQueuedUserInputEvents()) + retVal = true; + // If Qt is used as a plugin, or as an extension in a native cocoa // application, we should not run or stop NSApplication; This will be @@ -843,6 +836,23 @@ void QCocoaEventDispatcherPrivate::waitingObserverCallback(CFRunLoopObserverRef, emit static_cast(info)->awake(); } +bool QCocoaEventDispatcherPrivate::sendQueuedUserInputEvents() +{ + Q_Q(QCocoaEventDispatcher); + if (processEventsFlags & QEventLoop::ExcludeUserInputEvents) + return false; + bool didSendEvent = false; + while (!queuedUserInputEvents.isEmpty()) { + NSEvent *event = static_cast(queuedUserInputEvents.takeFirst()); + if (!q->filterNativeEvent("NSEvent", event, nullptr)) { + [NSApp sendEvent:event]; + didSendEvent = true; + } + [event release]; + } + return didSendEvent; +} + void QCocoaEventDispatcherPrivate::processPostedEvents() { if (blockSendPostedEvents) { @@ -896,6 +906,7 @@ void QCocoaEventDispatcherPrivate::postedEventsSourceCallback(void *info) d->maybeCancelWaitForMoreEvents(); return; } + d->sendQueuedUserInputEvents(); d->processPostedEvents(); d->maybeCancelWaitForMoreEvents(); } -- cgit v1.2.3 From 8e528d8bd08406e9cc86abcfe153f02d585d3654 Mon Sep 17 00:00:00 2001 From: Frederik Gladhorn Date: Tue, 21 May 2019 14:16:16 +0200 Subject: iOS Accessibility: implement accessibilityElements and check indexing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: QTBUG-70683 Change-Id: I122c67a5cee22363de5c8e45dc1c83e7760162fb Reviewed-by: Tor Arne Vestbø --- src/plugins/platforms/ios/quiview_accessibility.mm | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'src/plugins') diff --git a/src/plugins/platforms/ios/quiview_accessibility.mm b/src/plugins/platforms/ios/quiview_accessibility.mm index a3f4156a59..458ddcc9b8 100644 --- a/src/plugins/platforms/ios/quiview_accessibility.mm +++ b/src/plugins/platforms/ios/quiview_accessibility.mm @@ -101,6 +101,8 @@ - (id)accessibilityElementAtIndex:(NSInteger)index { [self initAccessibility]; + if (index >= [m_accessibleElements count]) + return nil; return m_accessibleElements[index]; } @@ -110,4 +112,10 @@ return [m_accessibleElements indexOfObject:element]; } +- (NSArray *)accessibilityElements +{ + [self initAccessibility]; + return m_accessibleElements; +} + @end -- cgit v1.2.3 From cf052e0737ecf2850b125c1640fe300699303b70 Mon Sep 17 00:00:00 2001 From: Tim Blechmann Date: Thu, 6 Dec 2018 15:39:57 +0800 Subject: Windows: Use UUIDs instead of function pointer to mangle window classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of using the address of a function pointer, we name-mangle window classes by using an UUID. This fixes a real-world problem with multiple Qt instances where for some reasons the window function appears to be mapped to the same address. Change-Id: Id27e8d7aa17a4db9c14559224395f49d3ecd8d78 Reviewed-by: Tor Arne Vestbø --- src/plugins/platforms/windows/qwindowscontext.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/windows/qwindowscontext.cpp b/src/plugins/platforms/windows/qwindowscontext.cpp index de533cab08..38b9823d6b 100644 --- a/src/plugins/platforms/windows/qwindowscontext.cpp +++ b/src/plugins/platforms/windows/qwindowscontext.cpp @@ -77,6 +77,7 @@ #include #include #include +#include #include #include @@ -544,7 +545,7 @@ QString QWindowsContext::registerWindowClass(QString cname, // each one has to have window class names with a unique name // The first instance gets the unmodified name; if the class // has already been registered by another instance of Qt then - // add an instance-specific ID, the address of the window proc. + // add a UUID. static int classExists = -1; const HINSTANCE appInstance = static_cast(GetModuleHandle(nullptr)); @@ -555,7 +556,7 @@ QString QWindowsContext::registerWindowClass(QString cname, } if (classExists) - cname += QString::number(reinterpret_cast(proc)); + cname += QUuid::createUuid().toString(); if (d->m_registeredWindowClassNames.contains(cname)) // already registered in our list return cname; -- cgit v1.2.3 From f344b6357e751f54bcb003bc88c7fe15d616be7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Arne=20Vestb=C3=B8?= Date: Tue, 18 Jun 2019 14:58:25 +0200 Subject: macOS: Invalidate window shadow after QNSWindowBackingStore resize The window shadow rendered by AppKit is based on the shape/content of the NSWindow surface. If the backingstore is partially transparent, we need to invalidate the window shadow after each resize (and subsequent flush) of the backingstore. Change-Id: I451370af5a8c0c25faea26beb3faa2483a33a5cf Fixes: QTBUG-74560 Reviewed-by: Timur Pocheptsov --- src/plugins/platforms/cocoa/qcocoabackingstore.h | 1 + src/plugins/platforms/cocoa/qcocoabackingstore.mm | 19 +++++++++++++++++++ src/plugins/platforms/cocoa/qcocoawindow.mm | 20 ++++++++++---------- 3 files changed, 30 insertions(+), 10 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.h b/src/plugins/platforms/cocoa/qcocoabackingstore.h index acddc3ecc8..2398e6351e 100644 --- a/src/plugins/platforms/cocoa/qcocoabackingstore.h +++ b/src/plugins/platforms/cocoa/qcocoabackingstore.h @@ -55,6 +55,7 @@ public: QNSWindowBackingStore(QWindow *window); ~QNSWindowBackingStore(); + void resize(const QSize &size, const QRegion &staticContents) override; void flush(QWindow *, const QRegion &, const QPoint &) override; private: diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.mm b/src/plugins/platforms/cocoa/qcocoabackingstore.mm index cff1f96615..233ccfab9e 100644 --- a/src/plugins/platforms/cocoa/qcocoabackingstore.mm +++ b/src/plugins/platforms/cocoa/qcocoabackingstore.mm @@ -71,6 +71,24 @@ QImage::Format QNSWindowBackingStore::format() const return QRasterBackingStore::format(); } +void QNSWindowBackingStore::resize(const QSize &size, const QRegion &staticContents) +{ + qCDebug(lcQpaBackingStore) << "Resize requested to" << size; + QRasterBackingStore::resize(size, staticContents); + + // The window shadow rendered by AppKit is based on the shape/content of the + // NSWindow surface. Technically any flush of the backingstore can result in + // a potentially new shape of the window, and would need a shadow invalidation, + // but this is likely too expensive to do at every flush for the few cases where + // clients change the shape dynamically. One case where we do know that the shadow + // likely needs invalidation, if the window has partially transparent content, + // is after a resize, where AppKit's default shadow may be based on the previous + // window content. + QCocoaWindow *cocoaWindow = static_cast(window()->handle()); + if (cocoaWindow->isContentView() && !cocoaWindow->isOpaque()) + cocoaWindow->m_needsInvalidateShadow = true; +} + /*! Flushes the given \a region from the specified \a window onto the screen. @@ -217,6 +235,7 @@ void QNSWindowBackingStore::flush(QWindow *window, const QRegion ®ion, const QCocoaWindow *topLevelCocoaWindow = static_cast(topLevelWindow->handle()); if (Q_UNLIKELY(topLevelCocoaWindow->m_needsInvalidateShadow)) { + qCDebug(lcQpaBackingStore) << "Invalidating window shadow for" << topLevelCocoaWindow; [topLevelView.window invalidateShadow]; topLevelCocoaWindow->m_needsInvalidateShadow = false; } diff --git a/src/plugins/platforms/cocoa/qcocoawindow.mm b/src/plugins/platforms/cocoa/qcocoawindow.mm index d0ad1791c3..dc7319484e 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.mm +++ b/src/plugins/platforms/cocoa/qcocoawindow.mm @@ -1010,16 +1010,16 @@ void QCocoaWindow::setMask(const QRegion ®ion) } else { m_view.layer.mask = nil; } - } - - if (isContentView()) { - // Setting the mask requires invalidating the NSWindow shadow, but that needs - // to happen after the backingstore has been redrawn, so that AppKit can pick - // up the new window shape based on the backingstore content. Doing a display - // directly here is not an option, as the window might not be exposed at this - // time, and so would not result in an updated backingstore. - m_needsInvalidateShadow = true; - [m_view setNeedsDisplay:YES]; + } else { + if (isContentView()) { + // Setting the mask requires invalidating the NSWindow shadow, but that needs + // to happen after the backingstore has been redrawn, so that AppKit can pick + // up the new window shape based on the backingstore content. Doing a display + // directly here is not an option, as the window might not be exposed at this + // time, and so would not result in an updated backingstore. + m_needsInvalidateShadow = true; + [m_view setNeedsDisplay:YES]; + } } } -- cgit v1.2.3 From 1975a98345d96b6155b651bba65eb8ae8ca30d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Johan=20S=C3=B8rvig?= Date: Wed, 19 Jun 2019 12:54:46 +0200 Subject: macOS: prevent duplicate backing store scaling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit m_requestedSize is already scaled by the QtGui scale factor (e.g. as set by QT_SCALE_FACTOR). Multiplying by QWindow::devicePixelRatio() then applies this factor again. Use QPlatformWindow::devicePixelRatio() instead, which returns the platform scale factor. Change-Id: I133e99d84f4718215fda9ef0cf81a113b51db2c7 Reviewed-by: Tor Arne Vestbø --- src/plugins/platforms/cocoa/qcocoabackingstore.mm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.mm b/src/plugins/platforms/cocoa/qcocoabackingstore.mm index 233ccfab9e..78aa98094c 100644 --- a/src/plugins/platforms/cocoa/qcocoabackingstore.mm +++ b/src/plugins/platforms/cocoa/qcocoabackingstore.mm @@ -413,10 +413,11 @@ void QCALayerBackingStore::ensureBackBuffer() bool QCALayerBackingStore::recreateBackBufferIfNeeded() { - const qreal devicePixelRatio = window()->devicePixelRatio(); + const QCocoaWindow *platformWindow = static_cast(window()->handle()); + const qreal devicePixelRatio = platformWindow->devicePixelRatio(); QSize requestedBufferSize = m_requestedSize * devicePixelRatio; - const NSView *backingStoreView = static_cast(window()->handle())->view(); + const NSView *backingStoreView = platformWindow->view(); Q_UNUSED(backingStoreView); auto bufferSizeMismatch = [&](const QSize requested, const QSize actual) { -- cgit v1.2.3 From d5adaacbb2ae6c1470ebf4b975968534a82225b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Arne=20Vestb=C3=B8?= Date: Wed, 19 Jun 2019 11:43:14 +0200 Subject: macOS: Make QMacStyle::standardPalette() reflect the platform theme This palette isn't usually used, and the platform theme's palette is preferred, but if a client asks for the standard palette (e.g. as used in the styles example) we should try to report a palette that gives the system look. Change-Id: Ie5e58c890c13c716a9e9b5093b954a737e550dee Reviewed-by: Timur Pocheptsov --- src/plugins/styles/mac/qmacstyle_mac.mm | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/styles/mac/qmacstyle_mac.mm b/src/plugins/styles/mac/qmacstyle_mac.mm index c1f88149bb..482adf0433 100644 --- a/src/plugins/styles/mac/qmacstyle_mac.mm +++ b/src/plugins/styles/mac/qmacstyle_mac.mm @@ -2555,11 +2555,12 @@ int QMacStyle::pixelMetric(PixelMetric metric, const QStyleOption *opt, const QW QPalette QMacStyle::standardPalette() const { - QPalette pal = QCommonStyle::standardPalette(); - pal.setColor(QPalette::Disabled, QPalette::Dark, QColor(191, 191, 191)); - pal.setColor(QPalette::Active, QPalette::Dark, QColor(191, 191, 191)); - pal.setColor(QPalette::Inactive, QPalette::Dark, QColor(191, 191, 191)); - return pal; + auto platformTheme = QGuiApplicationPrivate::platformTheme(); + auto styleNames = platformTheme->themeHint(QPlatformTheme::StyleNames); + if (styleNames.toStringList().contains("macintosh")) + return *platformTheme->palette(); + else + return QStyle::standardPalette(); } int QMacStyle::styleHint(StyleHint sh, const QStyleOption *opt, const QWidget *w, -- cgit v1.2.3