// 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 <QtCore/qdebug.h>
#include <QtCore/qobject.h>
#include <QtCore/qrect.h>
#include <QtCore/qtextboundaryfinder.h>

#include <QtGui/qevent.h>
#include <QtGui/qtextformat.h>
#include <QtGui/qpalette.h>
#include <QtGui/qguiapplication.h>

#include <private/qhighdpiscaling_p.h>

#include <algorithm>

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<quintptr>(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<QInputMethodEvent::Attribute>
    intermediateMarkup(int position, int compositionLength,
                       int selStart, int selLength)
{
    QList<QInputMethodEvent::Attribute> 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<QInputMethodEvent> 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<RECONVERTSTRING *>(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<QInputMethodEvent::Attribute> 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<ushort *>(reconv + 1);
    std::copy(surroundingText.utf16(), surroundingText.utf16() + surroundingText.size(),
              pastReconv);
    return memSize;
}

QT_END_NAMESPACE
