// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qwindowsinputcontext.h" #include "qwindowscontext.h" #include "qwindowswindow.h" #include "qwindowsintegration.h" #include "qwindowsmousehandler.h" #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE static inline QByteArray debugComposition(int lParam) { QByteArray str; if (lParam & GCS_RESULTSTR) str += "RESULTSTR "; if (lParam & GCS_COMPSTR) str += "COMPSTR "; if (lParam & GCS_COMPATTR) str += "COMPATTR "; if (lParam & GCS_CURSORPOS) str += "CURSORPOS "; if (lParam & GCS_COMPCLAUSE) str += "COMPCLAUSE "; if (lParam & CS_INSERTCHAR) str += "INSERTCHAR "; if (lParam & CS_NOMOVECARET) str += "NOMOVECARET "; return str; } // Cancel current IME composition. static inline void imeNotifyCancelComposition(HWND hwnd) { if (!hwnd) { qWarning() << __FUNCTION__ << "called with" << hwnd; return; } const HIMC himc = ImmGetContext(hwnd); ImmNotifyIME(himc, NI_COMPOSITIONSTR, CPS_CANCEL, 0); ImmReleaseContext(hwnd, himc); } static inline LCID languageIdFromLocaleId(LCID localeId) { return localeId & 0xFFFF; } static inline LCID currentInputLanguageId() { return languageIdFromLocaleId(reinterpret_cast(GetKeyboardLayout(0))); } Q_CORE_EXPORT QLocale qt_localeFromLCID(LCID id); // from qlocale_win.cpp /*! \class QWindowsInputContext \brief Windows Input context implementation Handles input of foreign characters (particularly East Asian) languages. \section1 Testing \list \li Install the East Asian language support and choose Japanese (say). \li Compile the \a mainwindows/mdi example and open a text window. \li In the language bar, switch to Japanese and choose the Input method 'Hiragana'. \li In a text editor control, type the syllable \a 'la'. Underlined characters show up, indicating that there is completion available. Press the Space key two times. A completion popup occurs which shows the options. \endlist Reconversion: Input texts can be 'converted' into different input modes or more completion suggestions can be made based on context to correct errors. This is bound to the 'Conversion key' (F13-key in Japanese, which can be changed in the configuration). After writing text, pressing the key selects text and triggers a conversion popup, which shows the alternatives for the word. \section1 Interaction When the user activates input methods, Windows sends WM_IME_STARTCOMPOSITION, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION messages that trigger startComposition(), composition(), endComposition(), respectively. No key events are sent. composition() determines the markup of the pre-edit or selected text and/or the final text and sends that to the focus object. In between startComposition(), endComposition(), multiple compositions may happen (isComposing). update() is called to synchronize the position of the candidate window with the microfocus rectangle of the focus object. Also, a hidden caret is moved along with that position, which is important for some Chinese input methods. reset() is called to cancel a composition if the mouse is moved outside or for example some Undo/Redo operation is invoked. \note Mouse interaction of popups with QtWindows::InputMethodOpenCandidateWindowEvent and QtWindows::InputMethodCloseCandidateWindowEvent needs to be checked (mouse grab might interfere with candidate window). \internal */ QWindowsInputContext::QWindowsInputContext() : m_WM_MSIME_MOUSE(RegisterWindowMessage(L"MSIMEMouseOperation")), m_languageId(currentInputLanguageId()), m_locale(qt_localeFromLCID(m_languageId)) { const quint32 bmpData = 0; m_transparentBitmap = CreateBitmap(2, 2, 1, 1, &bmpData); connect(QGuiApplication::inputMethod(), &QInputMethod::cursorRectangleChanged, this, &QWindowsInputContext::cursorRectChanged); } QWindowsInputContext::~QWindowsInputContext() { if (m_transparentBitmap) DeleteObject(m_transparentBitmap); } bool QWindowsInputContext::hasCapability(Capability capability) const { switch (capability) { case QPlatformInputContext::HiddenTextCapability: return false; // QTBUG-40691, do not show IME on desktop for password entry fields. default: break; } return true; } /*! \brief Cancels a composition. */ void QWindowsInputContext::reset() { if (!m_compositionContext.hwnd) return; qCDebug(lcQpaInputMethods) << __FUNCTION__; if (m_compositionContext.isComposing && !m_compositionContext.focusObject.isNull()) { QInputMethodEvent event; if (!m_compositionContext.composition.isEmpty()) event.setCommitString(m_compositionContext.composition); QCoreApplication::sendEvent(m_compositionContext.focusObject, &event); endContextComposition(); } imeNotifyCancelComposition(m_compositionContext.hwnd); doneContext(); } void QWindowsInputContext::setFocusObject(QObject *) { // ### fixme: On Windows 8.1, it has been observed that the Input context // remains active when this happens resulting in a lock-up. Consecutive // key events still have VK_PROCESSKEY set and are thus ignored. if (m_compositionContext.isComposing) reset(); updateEnabled(); } HWND QWindowsInputContext::getVirtualKeyboardWindowHandle() const { return ::FindWindowA("IPTip_Main_Window", nullptr); } QRectF QWindowsInputContext::keyboardRect() const { if (HWND hwnd = getVirtualKeyboardWindowHandle()) { RECT rect; if (::GetWindowRect(hwnd, &rect)) { return QRectF(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top); } } return QRectF(); } bool QWindowsInputContext::isInputPanelVisible() const { HWND hwnd = getVirtualKeyboardWindowHandle(); if (hwnd && ::IsWindowEnabled(hwnd) && ::IsWindowVisible(hwnd)) return true; // check if the Input Method Editor is open if (inputMethodAccepted()) { if (QWindow *window = QGuiApplication::focusWindow()) { if (QWindowsWindow *platformWindow = QWindowsWindow::windowsWindowOf(window)) { if (HIMC himc = ImmGetContext(platformWindow->handle())) return ImmGetOpenStatus(himc); } } } return false; } void QWindowsInputContext::showInputPanel() { if (!inputMethodAccepted()) return; QWindow *window = QGuiApplication::focusWindow(); if (!window) return; QWindowsWindow *platformWindow = QWindowsWindow::windowsWindowOf(window); if (!platformWindow) return; // Create an invisible 2x2 caret, which will be kept at the microfocus position. // It is important for triggering the on-screen keyboard in touch-screen devices, // for some Chinese input methods, and for Magnifier's "follow keyboard" feature. if (!m_caretCreated && m_transparentBitmap) m_caretCreated = CreateCaret(platformWindow->handle(), m_transparentBitmap, 0, 0); if (m_caretCreated) { cursorRectChanged(); ShowCaret(platformWindow->handle()); } } void QWindowsInputContext::hideInputPanel() { if (m_caretCreated) { DestroyCaret(); m_caretCreated = false; } } void QWindowsInputContext::updateEnabled() { if (!QGuiApplication::focusObject()) return; if (QWindowsWindow *platformWindow = QWindowsWindow::windowsWindowOf(QGuiApplication::focusWindow())) { const bool accepted = inputMethodAccepted(); if (QWindowsContext::verbose > 1) qCDebug(lcQpaInputMethods) << __FUNCTION__ << platformWindow->window() << "accepted=" << accepted; QWindowsInputContext::setWindowsImeEnabled(platformWindow, accepted); } } void QWindowsInputContext::setWindowsImeEnabled(QWindowsWindow *platformWindow, bool enabled) { if (!platformWindow) return; if (enabled) { // Re-enable Windows IME by associating default context. ImmAssociateContextEx(platformWindow->handle(), nullptr, IACE_DEFAULT); } else { // Disable Windows IME by associating 0 context. ImmAssociateContext(platformWindow->handle(), nullptr); } } /*! \brief Moves the candidate window along with microfocus of the focus object. */ void QWindowsInputContext::update(Qt::InputMethodQueries queries) { if (queries & Qt::ImEnabled) updateEnabled(); } void QWindowsInputContext::cursorRectChanged() { QWindow *window = QGuiApplication::focusWindow(); if (!window) return; qreal factor = QHighDpiScaling::factor(window); const QInputMethod *inputMethod = QGuiApplication::inputMethod(); const QRectF cursorRectangleF = inputMethod->cursorRectangle(); if (!cursorRectangleF.isValid()) return; const QRect cursorRectangle = QRectF(cursorRectangleF.topLeft() * factor, cursorRectangleF.size() * factor).toRect(); if (m_caretCreated) SetCaretPos(cursorRectangle.x(), cursorRectangle.y()); if (!m_compositionContext.hwnd) return; qCDebug(lcQpaInputMethods) << __FUNCTION__<< cursorRectangle; const HIMC himc = ImmGetContext(m_compositionContext.hwnd); if (!himc) return; // Move candidate list window to the microfocus position. COMPOSITIONFORM cf; // ### need X-like inputStyle config settings cf.dwStyle = CFS_FORCE_POSITION; cf.ptCurrentPos.x = cursorRectangle.x(); cf.ptCurrentPos.y = cursorRectangle.y(); CANDIDATEFORM candf; candf.dwIndex = 0; candf.dwStyle = CFS_EXCLUDE; candf.ptCurrentPos.x = cursorRectangle.x(); candf.ptCurrentPos.y = cursorRectangle.y() + cursorRectangle.height(); candf.rcArea.left = cursorRectangle.x(); candf.rcArea.top = cursorRectangle.y(); candf.rcArea.right = cursorRectangle.x() + cursorRectangle.width(); candf.rcArea.bottom = cursorRectangle.y() + cursorRectangle.height(); ImmSetCompositionWindow(himc, &cf); ImmSetCandidateWindow(himc, &candf); ImmReleaseContext(m_compositionContext.hwnd, himc); } void QWindowsInputContext::invokeAction(QInputMethod::Action action, int cursorPosition) { if (action != QInputMethod::Click || !m_compositionContext.hwnd) { QPlatformInputContext::invokeAction(action, cursorPosition); return; } qCDebug(lcQpaInputMethods) << __FUNCTION__ << cursorPosition << action; if (cursorPosition < 0 || cursorPosition > m_compositionContext.composition.size()) reset(); // Magic code that notifies Japanese IME about the cursor // position. const HIMC himc = ImmGetContext(m_compositionContext.hwnd); const HWND imeWindow = ImmGetDefaultIMEWnd(m_compositionContext.hwnd); const WPARAM mouseOperationCode = MAKELONG(MAKEWORD(MK_LBUTTON, cursorPosition == 0 ? 2 : 1), cursorPosition); SendMessage(imeWindow, m_WM_MSIME_MOUSE, mouseOperationCode, LPARAM(himc)); ImmReleaseContext(m_compositionContext.hwnd, himc); } static inline QString getCompositionString(HIMC himc, DWORD dwIndex) { enum { bufferSize = 256 }; wchar_t buffer[bufferSize]; const int length = ImmGetCompositionString(himc, dwIndex, buffer, bufferSize * sizeof(wchar_t)); return QString::fromWCharArray(buffer, size_t(length) / sizeof(wchar_t)); } // Determine the converted string range as pair of start/length to be selected. static inline void getCompositionStringConvertedRange(HIMC himc, int *selStart, int *selLength) { enum { bufferSize = 256 }; // Find the range of bytes with ATTR_TARGET_CONVERTED set. char attrBuffer[bufferSize]; *selStart = *selLength = 0; if (const int attrLength = ImmGetCompositionString(himc, GCS_COMPATTR, attrBuffer, bufferSize)) { int start = 0; while (start < attrLength && !(attrBuffer[start] & ATTR_TARGET_CONVERTED)) start++; if (start < attrLength) { int end = start + 1; while (end < attrLength && (attrBuffer[end] & ATTR_TARGET_CONVERTED)) end++; *selStart = start; *selLength = end - start; } } } enum StandardFormat { PreeditFormat, SelectionFormat }; static inline QTextFormat standardFormat(StandardFormat format) { QTextCharFormat result; switch (format) { case PreeditFormat: result.setUnderlineStyle(QTextCharFormat::DashUnderline); break; case SelectionFormat: { // TODO: Should be that of the widget? const QPalette palette = QGuiApplication::palette(); const QColor background = palette.text().color(); result.setBackground(QBrush(background)); result.setForeground(palette.window()); break; } } return result; } bool QWindowsInputContext::startComposition(HWND hwnd) { QObject *fo = QGuiApplication::focusObject(); if (!fo) return false; // This should always match the object. QWindow *window = QGuiApplication::focusWindow(); if (!window) return false; qCDebug(lcQpaInputMethods) << __FUNCTION__ << fo << window << "language=" << m_languageId; if (!fo || QWindowsWindow::handleOf(window) != hwnd) return false; initContext(hwnd, fo); startContextComposition(); return true; } void QWindowsInputContext::startContextComposition() { if (m_compositionContext.isComposing) { qWarning("%s: Called out of sequence.", __FUNCTION__); return; } m_compositionContext.isComposing = true; m_compositionContext.composition.clear(); m_compositionContext.position = 0; cursorRectChanged(); // position cursor initially. update(Qt::ImQueryAll); } void QWindowsInputContext::endContextComposition() { if (!m_compositionContext.isComposing) { qWarning("%s: Called out of sequence.", __FUNCTION__); return; } m_compositionContext.composition.clear(); m_compositionContext.position = 0; m_compositionContext.isComposing = false; } // Create a list of markup attributes for QInputMethodEvent // to display the selected part of the intermediate composition // result differently. static inline QList intermediateMarkup(int position, int compositionLength, int selStart, int selLength) { QList attributes; if (selStart > 0) attributes << QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, 0, selStart, standardFormat(PreeditFormat)); if (selLength) attributes << QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, selStart, selLength, standardFormat(SelectionFormat)); if (selStart + selLength < compositionLength) attributes << QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, selStart + selLength, compositionLength - selStart - selLength, standardFormat(PreeditFormat)); if (position >= 0) attributes << QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, position, selLength ? 0 : 1, QVariant()); return attributes; } /*! \brief Notify focus object about markup or final text. */ bool QWindowsInputContext::composition(HWND hwnd, LPARAM lParamIn) { const int lParam = int(lParamIn); qCDebug(lcQpaInputMethods) << '>' << __FUNCTION__ << m_compositionContext.focusObject << debugComposition(lParam) << " composing=" << m_compositionContext.isComposing; if (m_compositionContext.focusObject.isNull() || m_compositionContext.hwnd != hwnd || !lParam) return false; const HIMC himc = ImmGetContext(m_compositionContext.hwnd); if (!himc) return false; QScopedPointer event; if (lParam & (GCS_COMPSTR | GCS_COMPATTR | GCS_CURSORPOS)) { if (!m_compositionContext.isComposing) startContextComposition(); // Some intermediate composition result. Parametrize event with // attribute sequence specifying the formatting of the converted part. int selStart, selLength; m_compositionContext.composition = getCompositionString(himc, GCS_COMPSTR); m_compositionContext.position = ImmGetCompositionString(himc, GCS_CURSORPOS, nullptr, 0); getCompositionStringConvertedRange(himc, &selStart, &selLength); if ((lParam & CS_INSERTCHAR) && (lParam & CS_NOMOVECARET)) { // make Korean work correctly. Hope this is correct for all IMEs selStart = 0; selLength = m_compositionContext.composition.size(); } if (!selLength) selStart = 0; event.reset(new QInputMethodEvent(m_compositionContext.composition, intermediateMarkup(m_compositionContext.position, m_compositionContext.composition.size(), selStart, selLength))); } if (event.isNull()) event.reset(new QInputMethodEvent); if (lParam & GCS_RESULTSTR) { // A fixed result, return the converted string event->setCommitString(getCompositionString(himc, GCS_RESULTSTR)); if (!(lParam & GCS_DELTASTART)) endContextComposition(); } const bool result = QCoreApplication::sendEvent(m_compositionContext.focusObject, event.data()); qCDebug(lcQpaInputMethods) << '<' << __FUNCTION__ << "sending markup=" << event->attributes().size() << " commit=" << event->commitString() << " to " << m_compositionContext.focusObject << " returns " << result; update(Qt::ImQueryAll); ImmReleaseContext(m_compositionContext.hwnd, himc); return result; } bool QWindowsInputContext::endComposition(HWND hwnd) { qCDebug(lcQpaInputMethods) << __FUNCTION__ << m_endCompositionRecursionGuard << hwnd; // Googles Pinyin Input Method likes to call endComposition again // when we call notifyIME with CPS_CANCEL, so protect ourselves // against that. if (m_endCompositionRecursionGuard || m_compositionContext.hwnd != hwnd) return false; if (m_compositionContext.focusObject.isNull()) return false; // QTBUG-58300: Ignore WM_IME_ENDCOMPOSITION when CTRL is pressed to prevent // for example the text being cleared when pressing CTRL+A if (m_locale.language() == QLocale::Korean && QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ControlModifier)) { reset(); return true; } m_endCompositionRecursionGuard = true; imeNotifyCancelComposition(m_compositionContext.hwnd); if (m_compositionContext.isComposing) { QInputMethodEvent event; QCoreApplication::sendEvent(m_compositionContext.focusObject, &event); } doneContext(); m_endCompositionRecursionGuard = false; return true; } void QWindowsInputContext::initContext(HWND hwnd, QObject *focusObject) { if (m_compositionContext.hwnd) doneContext(); m_compositionContext.hwnd = hwnd; m_compositionContext.focusObject = focusObject; update(Qt::ImQueryAll); m_compositionContext.isComposing = false; m_compositionContext.position = 0; } void QWindowsInputContext::doneContext() { if (!m_compositionContext.hwnd) return; m_compositionContext.hwnd = nullptr; m_compositionContext.composition.clear(); m_compositionContext.position = 0; m_compositionContext.isComposing = false; m_compositionContext.focusObject = nullptr; } bool QWindowsInputContext::handleIME_Request(WPARAM wParam, LPARAM lParam, LRESULT *result) { switch (int(wParam)) { case IMR_RECONVERTSTRING: { const int size = reconvertString(reinterpret_cast(lParam)); if (size < 0) return false; *result = size; } return true; case IMR_CONFIRMRECONVERTSTRING: return true; default: break; } return false; } void QWindowsInputContext::handleInputLanguageChanged(WPARAM wparam, LPARAM lparam) { const LCID newLanguageId = languageIdFromLocaleId(WORD(lparam)); if (newLanguageId == m_languageId) return; const LCID oldLanguageId = m_languageId; m_languageId = newLanguageId; m_locale = qt_localeFromLCID(m_languageId); emitLocaleChanged(); qCDebug(lcQpaInputMethods) << __FUNCTION__ << Qt::hex << Qt::showbase << oldLanguageId << "->" << newLanguageId << "Character set:" << DWORD(wparam) << Qt::dec << Qt::noshowbase << m_locale; } /*! \brief Determines the string for reconversion with selection. This is triggered twice by WM_IME_REQUEST, first with reconv=0 to determine the length and later with a reconv struct to obtain the string with the position of the selection to be reconverted. Obtains the text from the focus object and marks the word for selection (might not be entirely correct for Japanese). */ int QWindowsInputContext::reconvertString(RECONVERTSTRING *reconv) { QObject *fo = QGuiApplication::focusObject(); if (!fo) return false; const QVariant surroundingTextV = QInputMethod::queryFocusObject(Qt::ImSurroundingText, QVariant()); if (!surroundingTextV.isValid()) return -1; const QString surroundingText = surroundingTextV.toString(); const int memSize = int(sizeof(RECONVERTSTRING)) + (surroundingText.length() + 1) * int(sizeof(ushort)); qCDebug(lcQpaInputMethods) << __FUNCTION__ << " reconv=" << reconv << " surroundingText=" << surroundingText << " size=" << memSize; // If memory is not allocated, return the required size. if (!reconv) return surroundingText.isEmpty() ? -1 : memSize; const QVariant posV = QInputMethod::queryFocusObject(Qt::ImCursorPosition, QVariant()); const int pos = posV.isValid() ? posV.toInt() : 0; // Find the word in the surrounding text. QTextBoundaryFinder bounds(QTextBoundaryFinder::Word, surroundingText); bounds.setPosition(pos); if (bounds.position() > 0 && !(bounds.boundaryReasons() & QTextBoundaryFinder::StartOfItem)) bounds.toPreviousBoundary(); const int startPos = bounds.position(); bounds.toNextBoundary(); const int endPos = bounds.position(); qCDebug(lcQpaInputMethods) << __FUNCTION__ << " boundary=" << startPos << endPos; // Select the text, this will be overwritten by following IME events. QList attributes; attributes << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, startPos, endPos-startPos, QVariant()); QInputMethodEvent selectEvent(QString(), attributes); QCoreApplication::sendEvent(fo, &selectEvent); reconv->dwSize = DWORD(memSize); reconv->dwVersion = 0; reconv->dwStrLen = DWORD(surroundingText.size()); reconv->dwStrOffset = sizeof(RECONVERTSTRING); reconv->dwCompStrLen = DWORD(endPos - startPos); // TCHAR count. reconv->dwCompStrOffset = DWORD(startPos) * sizeof(ushort); // byte count. reconv->dwTargetStrLen = reconv->dwCompStrLen; reconv->dwTargetStrOffset = reconv->dwCompStrOffset; auto *pastReconv = reinterpret_cast(reconv + 1); std::copy(surroundingText.utf16(), surroundingText.utf16() + surroundingText.size(), pastReconv); return memSize; } QT_END_NAMESPACE