| /**************************************************************************** |
| ** |
| ** Copyright (C) 2017-2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtWaylandCompositor module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:GPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 3 or (at your option) any later version |
| ** approved by the KDE Free Qt Foundation. The licenses are as published by |
| ** the Free Software Foundation and appearing in the file LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qwaylandtextinput.h" |
| #include "qwaylandtextinput_p.h" |
| |
| #include <QtWaylandCompositor/QWaylandCompositor> |
| #include <QtWaylandCompositor/private/qwaylandseat_p.h> |
| |
| #include "qwaylandsurface.h" |
| #include "qwaylandview.h" |
| #include "qwaylandinputmethodeventbuilder_p.h" |
| |
| #include <QGuiApplication> |
| #include <QInputMethodEvent> |
| |
| #if QT_CONFIG(xkbcommon) |
| #include <QtXkbCommonSupport/private/qxkbcommon_p.h> |
| #endif |
| |
| QT_BEGIN_NAMESPACE |
| |
| QWaylandTextInputClientState::QWaylandTextInputClientState() |
| { |
| } |
| |
| Qt::InputMethodQueries QWaylandTextInputClientState::updatedQueries(const QWaylandTextInputClientState &other) const |
| { |
| Qt::InputMethodQueries queries; |
| |
| if (hints != other.hints) |
| queries |= Qt::ImHints; |
| if (cursorRectangle != other.cursorRectangle) |
| queries |= Qt::ImCursorRectangle; |
| if (surroundingText != other.surroundingText) |
| queries |= Qt::ImSurroundingText | Qt::ImCurrentSelection; |
| if (cursorPosition != other.cursorPosition) |
| queries |= Qt::ImCursorPosition | Qt::ImCurrentSelection; |
| if (anchorPosition != other.anchorPosition) |
| queries |= Qt::ImAnchorPosition | Qt::ImCurrentSelection; |
| if (preferredLanguage != other.preferredLanguage) |
| queries |= Qt::ImPreferredLanguage; |
| |
| return queries; |
| } |
| |
| Qt::InputMethodQueries QWaylandTextInputClientState::mergeChanged(const QWaylandTextInputClientState &other) { |
| Qt::InputMethodQueries queries; |
| |
| if ((other.changedState & Qt::ImHints) && hints != other.hints) { |
| hints = other.hints; |
| queries |= Qt::ImHints; |
| } |
| |
| if ((other.changedState & Qt::ImCursorRectangle) && cursorRectangle != other.cursorRectangle) { |
| cursorRectangle = other.cursorRectangle; |
| queries |= Qt::ImCursorRectangle; |
| } |
| |
| if ((other.changedState & Qt::ImSurroundingText) && surroundingText != other.surroundingText) { |
| surroundingText = other.surroundingText; |
| queries |= Qt::ImSurroundingText | Qt::ImCurrentSelection; |
| } |
| |
| if ((other.changedState & Qt::ImCursorPosition) && cursorPosition != other.cursorPosition) { |
| cursorPosition = other.cursorPosition; |
| queries |= Qt::ImCursorPosition | Qt::ImCurrentSelection; |
| } |
| |
| if ((other.changedState & Qt::ImAnchorPosition) && anchorPosition != other.anchorPosition) { |
| anchorPosition = other.anchorPosition; |
| queries |= Qt::ImAnchorPosition | Qt::ImCurrentSelection; |
| } |
| |
| if ((other.changedState & Qt::ImPreferredLanguage) && preferredLanguage != other.preferredLanguage) { |
| preferredLanguage = other.preferredLanguage; |
| queries |= Qt::ImPreferredLanguage; |
| } |
| |
| return queries; |
| } |
| |
| QWaylandTextInputPrivate::QWaylandTextInputPrivate(QWaylandCompositor *compositor) |
| : compositor(compositor) |
| , currentState(new QWaylandTextInputClientState) |
| , pendingState(new QWaylandTextInputClientState) |
| { |
| } |
| |
| void QWaylandTextInputPrivate::sendInputMethodEvent(QInputMethodEvent *event) |
| { |
| Q_Q(QWaylandTextInput); |
| |
| if (!focusResource || !focusResource->handle) |
| return; |
| |
| QWaylandTextInputClientState afterCommit; |
| |
| afterCommit.surroundingText = currentState->surroundingText; |
| afterCommit.cursorPosition = qMin(currentState->cursorPosition, currentState->anchorPosition); |
| |
| // Remove selection |
| afterCommit.surroundingText.remove(afterCommit.cursorPosition, qAbs(currentState->cursorPosition - currentState->anchorPosition)); |
| |
| if (event->replacementLength() > 0 || event->replacementStart() != 0) { |
| // Remove replacement |
| afterCommit.cursorPosition = qBound(0, afterCommit.cursorPosition + event->replacementStart(), afterCommit.surroundingText.length()); |
| afterCommit.surroundingText.remove(afterCommit.cursorPosition, |
| qMin(event->replacementLength(), |
| afterCommit.surroundingText.length() - afterCommit.cursorPosition)); |
| |
| if (event->replacementStart() <= 0 && (event->replacementLength() >= -event->replacementStart())) { |
| const int selectionStart = qMin(currentState->cursorPosition, currentState->anchorPosition); |
| const int selectionEnd = qMax(currentState->cursorPosition, currentState->anchorPosition); |
| const int before = QWaylandInputMethodEventBuilder::indexToWayland(currentState->surroundingText, -event->replacementStart(), selectionStart + event->replacementStart()); |
| const int after = QWaylandInputMethodEventBuilder::indexToWayland(currentState->surroundingText, event->replacementLength() + event->replacementStart(), selectionEnd); |
| send_delete_surrounding_text(focusResource->handle, before, after); |
| } else { |
| // TODO: Implement this case |
| qWarning() << "Not yet supported case of replacement. Start:" << event->replacementStart() << "length:" << event->replacementLength(); |
| } |
| } |
| |
| // Insert commit string |
| afterCommit.surroundingText.insert(afterCommit.cursorPosition, event->commitString()); |
| afterCommit.cursorPosition += event->commitString().length(); |
| afterCommit.anchorPosition = afterCommit.cursorPosition; |
| |
| for (const QInputMethodEvent::Attribute &attribute : event->attributes()) { |
| if (attribute.type == QInputMethodEvent::Selection) { |
| afterCommit.cursorPosition = attribute.start; |
| afterCommit.anchorPosition = attribute.length; |
| int cursor = QWaylandInputMethodEventBuilder::indexToWayland(afterCommit.surroundingText, qAbs(attribute.start - afterCommit.cursorPosition), qMin(attribute.start, afterCommit.cursorPosition)); |
| int anchor = QWaylandInputMethodEventBuilder::indexToWayland(afterCommit.surroundingText, qAbs(attribute.length - afterCommit.cursorPosition), qMin(attribute.length, afterCommit.cursorPosition)); |
| send_cursor_position(focusResource->handle, |
| attribute.start < afterCommit.cursorPosition ? -cursor : cursor, |
| attribute.length < afterCommit.cursorPosition ? -anchor : anchor); |
| } |
| } |
| send_commit_string(focusResource->handle, event->commitString()); |
| for (const QInputMethodEvent::Attribute &attribute : event->attributes()) { |
| if (attribute.type == QInputMethodEvent::Cursor) { |
| int index = QWaylandInputMethodEventBuilder::indexToWayland(event->preeditString(), attribute.start); |
| send_preedit_cursor(focusResource->handle, index); |
| } else if (attribute.type == QInputMethodEvent::TextFormat) { |
| int start = QWaylandInputMethodEventBuilder::indexToWayland(event->preeditString(), attribute.start); |
| int length = QWaylandInputMethodEventBuilder::indexToWayland(event->preeditString(), attribute.length, attribute.start); |
| // TODO add support for different stylesQWaylandTextInput |
| send_preedit_styling(focusResource->handle, start, length, preedit_style_default); |
| } |
| } |
| send_preedit_string(focusResource->handle, event->preeditString(), event->preeditString()); |
| |
| Qt::InputMethodQueries queries = currentState->updatedQueries(afterCommit); |
| currentState->surroundingText = afterCommit.surroundingText; |
| currentState->cursorPosition = afterCommit.cursorPosition; |
| currentState->anchorPosition = afterCommit.anchorPosition; |
| |
| if (queries) { |
| qCDebug(qLcWaylandCompositorInputMethods) << "QInputMethod::update() after QInputMethodEvent" << queries; |
| |
| emit q->updateInputMethod(queries); |
| } |
| } |
| |
| void QWaylandTextInputPrivate::sendKeyEvent(QKeyEvent *event) |
| { |
| if (!focusResource || !focusResource->handle) |
| return; |
| |
| // TODO add support for modifiers |
| |
| #if QT_CONFIG(xkbcommon) |
| for (xkb_keysym_t keysym : QXkbCommon::toKeysym(event)) { |
| send_keysym(focusResource->handle, event->timestamp(), keysym, |
| event->type() == QEvent::KeyPress ? WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED, |
| 0); |
| } |
| #else |
| Q_UNUSED(event); |
| #endif |
| } |
| |
| void QWaylandTextInputPrivate::sendInputPanelState() |
| { |
| if (!focusResource || !focusResource->handle) |
| return; |
| |
| QInputMethod *inputMethod = qApp->inputMethod(); |
| const QRectF& keyboardRect = inputMethod->keyboardRectangle(); |
| const QRectF& sceneInputRect = inputMethod->inputItemTransform().mapRect(inputMethod->inputItemRectangle()); |
| const QRectF& localRect = sceneInputRect.intersected(keyboardRect).translated(-sceneInputRect.topLeft()); |
| |
| send_input_panel_state(focusResource->handle, |
| inputMethod->isVisible() ? input_panel_visibility_visible : input_panel_visibility_hidden, |
| localRect.x(), localRect.y(), localRect.width(), localRect.height()); |
| } |
| |
| void QWaylandTextInputPrivate::sendTextDirection() |
| { |
| if (!focusResource || !focusResource->handle) |
| return; |
| |
| const Qt::LayoutDirection direction = qApp->inputMethod()->inputDirection(); |
| send_text_direction(focusResource->handle, |
| (direction == Qt::LeftToRight) ? text_direction_ltr : |
| (direction == Qt::RightToLeft) ? text_direction_rtl : text_direction_auto); |
| } |
| |
| void QWaylandTextInputPrivate::sendLocale() |
| { |
| if (!focusResource || !focusResource->handle) |
| return; |
| |
| const QLocale locale = qApp->inputMethod()->locale(); |
| send_language(focusResource->handle, locale.bcp47Name()); |
| } |
| |
| QVariant QWaylandTextInputPrivate::inputMethodQuery(Qt::InputMethodQuery property, QVariant argument) const |
| { |
| switch (property) { |
| case Qt::ImHints: |
| return QVariant(static_cast<int>(currentState->hints)); |
| case Qt::ImCursorRectangle: |
| return currentState->cursorRectangle; |
| case Qt::ImFont: |
| // Not supported |
| return QVariant(); |
| case Qt::ImCursorPosition: |
| return currentState->cursorPosition; |
| case Qt::ImSurroundingText: |
| return currentState->surroundingText; |
| case Qt::ImCurrentSelection: |
| return currentState->surroundingText.mid(qMin(currentState->cursorPosition, currentState->anchorPosition), |
| qAbs(currentState->anchorPosition - currentState->cursorPosition)); |
| case Qt::ImMaximumTextLength: |
| // Not supported |
| return QVariant(); |
| case Qt::ImAnchorPosition: |
| return currentState->anchorPosition; |
| case Qt::ImAbsolutePosition: |
| // We assume the surrounding text is our whole document for now |
| return currentState->cursorPosition; |
| case Qt::ImTextAfterCursor: |
| if (argument.isValid()) |
| return currentState->surroundingText.mid(currentState->cursorPosition, argument.toInt()); |
| return currentState->surroundingText.mid(currentState->cursorPosition); |
| case Qt::ImTextBeforeCursor: |
| if (argument.isValid()) |
| return currentState->surroundingText.left(currentState->cursorPosition).right(argument.toInt()); |
| return currentState->surroundingText.left(currentState->cursorPosition); |
| case Qt::ImPreferredLanguage: |
| return currentState->preferredLanguage; |
| |
| default: |
| return QVariant(); |
| } |
| } |
| |
| void QWaylandTextInputPrivate::setFocus(QWaylandSurface *surface) |
| { |
| Q_Q(QWaylandTextInput); |
| |
| if (focusResource && focus != surface) { |
| uint32_t serial = compositor->nextSerial(); |
| send_leave(focusResource->handle, serial, focus->resource()); |
| focusDestroyListener.reset(); |
| } |
| |
| Resource *resource = surface ? resourceMap().value(surface->waylandClient()) : 0; |
| |
| if (resource && (focus != surface || focusResource != resource)) { |
| uint32_t serial = compositor->nextSerial(); |
| currentState.reset(new QWaylandTextInputClientState); |
| pendingState.reset(new QWaylandTextInputClientState); |
| send_enter(resource->handle, serial, surface->resource()); |
| focusResource = resource; |
| sendInputPanelState(); |
| sendLocale(); |
| sendTextDirection(); |
| focusDestroyListener.listenForDestruction(surface->resource()); |
| if (inputPanelVisible && q->isSurfaceEnabled(surface)) |
| qApp->inputMethod()->show(); |
| } |
| |
| focusResource = resource; |
| focus = surface; |
| } |
| |
| void QWaylandTextInputPrivate::zwp_text_input_v2_bind_resource(Resource *resource) |
| { |
| send_modifiers_map(resource->handle, QByteArray("")); |
| } |
| |
| void QWaylandTextInputPrivate::zwp_text_input_v2_destroy_resource(Resource *resource) |
| { |
| if (focusResource == resource) |
| focusResource = nullptr; |
| } |
| |
| void QWaylandTextInputPrivate::zwp_text_input_v2_destroy(Resource *resource) |
| { |
| wl_resource_destroy(resource->handle); |
| } |
| |
| void QWaylandTextInputPrivate::zwp_text_input_v2_enable(Resource *resource, wl_resource *surface) |
| { |
| Q_Q(QWaylandTextInput); |
| |
| QWaylandSurface *s = QWaylandSurface::fromResource(surface); |
| enabledSurfaces.insert(resource, s); |
| emit q->surfaceEnabled(s); |
| } |
| |
| void QWaylandTextInputPrivate::zwp_text_input_v2_disable(QtWaylandServer::zwp_text_input_v2::Resource *resource, wl_resource *) |
| { |
| Q_Q(QWaylandTextInput); |
| |
| QWaylandSurface *s = enabledSurfaces.take(resource); |
| emit q->surfaceDisabled(s); |
| } |
| |
| void QWaylandTextInputPrivate::zwp_text_input_v2_show_input_panel(Resource *) |
| { |
| inputPanelVisible = true; |
| |
| qApp->inputMethod()->show(); |
| } |
| |
| void QWaylandTextInputPrivate::zwp_text_input_v2_hide_input_panel(Resource *) |
| { |
| inputPanelVisible = false; |
| |
| qApp->inputMethod()->hide(); |
| } |
| |
| void QWaylandTextInputPrivate::zwp_text_input_v2_set_cursor_rectangle(Resource *resource, int32_t x, int32_t y, int32_t width, int32_t height) |
| { |
| if (resource != focusResource) |
| return; |
| |
| pendingState->cursorRectangle = QRect(x, y, width, height); |
| |
| pendingState->changedState |= Qt::ImCursorRectangle; |
| } |
| |
| void QWaylandTextInputPrivate::zwp_text_input_v2_update_state(Resource *resource, uint32_t serial, uint32_t flags) |
| { |
| Q_Q(QWaylandTextInput); |
| |
| qCDebug(qLcWaylandCompositorInputMethods) << "update_state" << serial << flags; |
| |
| if (resource != focusResource) |
| return; |
| |
| if (flags == update_state_reset || flags == update_state_enter) { |
| qCDebug(qLcWaylandCompositorInputMethods) << "QInputMethod::reset()"; |
| qApp->inputMethod()->reset(); |
| } |
| |
| this->serial = serial; |
| |
| Qt::InputMethodQueries queries; |
| if (flags == update_state_change) { |
| queries = currentState->mergeChanged(*pendingState.data()); |
| } else { |
| queries = pendingState->updatedQueries(*currentState.data()); |
| currentState.swap(pendingState); |
| } |
| |
| pendingState.reset(new QWaylandTextInputClientState); |
| |
| if (queries) { |
| qCDebug(qLcWaylandCompositorInputMethods) << "QInputMethod::update()" << queries; |
| |
| emit q->updateInputMethod(queries); |
| } |
| } |
| |
| void QWaylandTextInputPrivate::zwp_text_input_v2_set_content_type(Resource *resource, uint32_t hint, uint32_t purpose) |
| { |
| if (resource != focusResource) |
| return; |
| |
| pendingState->hints = Qt::ImhNone; |
| |
| if ((hint & content_hint_auto_completion) == 0 |
| && (hint & content_hint_auto_correction) == 0) |
| pendingState->hints |= Qt::ImhNoPredictiveText; |
| if ((hint & content_hint_auto_capitalization) == 0) |
| pendingState->hints |= Qt::ImhNoAutoUppercase; |
| if ((hint & content_hint_lowercase) != 0) |
| pendingState->hints |= Qt::ImhPreferLowercase; |
| if ((hint & content_hint_uppercase) != 0) |
| pendingState->hints |= Qt::ImhPreferUppercase; |
| if ((hint & content_hint_hidden_text) != 0) |
| pendingState->hints |= Qt::ImhHiddenText; |
| if ((hint & content_hint_sensitive_data) != 0) |
| pendingState->hints |= Qt::ImhSensitiveData; |
| if ((hint & content_hint_latin) != 0) |
| pendingState->hints |= Qt::ImhLatinOnly; |
| if ((hint & content_hint_multiline) != 0) |
| pendingState->hints |= Qt::ImhMultiLine; |
| |
| switch (purpose) { |
| case content_purpose_normal: |
| break; |
| case content_purpose_alpha: |
| pendingState->hints |= Qt::ImhUppercaseOnly | Qt::ImhLowercaseOnly; |
| break; |
| case content_purpose_digits: |
| pendingState->hints |= Qt::ImhDigitsOnly; |
| break; |
| case content_purpose_number: |
| pendingState->hints |= Qt::ImhFormattedNumbersOnly; |
| break; |
| case content_purpose_phone: |
| pendingState->hints |= Qt::ImhDialableCharactersOnly; |
| break; |
| case content_purpose_url: |
| pendingState->hints |= Qt::ImhUrlCharactersOnly; |
| break; |
| case content_purpose_email: |
| pendingState->hints |= Qt::ImhEmailCharactersOnly; |
| break; |
| case content_purpose_name: |
| case content_purpose_password: |
| break; |
| case content_purpose_date: |
| pendingState->hints |= Qt::ImhDate; |
| break; |
| case content_purpose_time: |
| pendingState->hints |= Qt::ImhTime; |
| break; |
| case content_purpose_datetime: |
| pendingState->hints |= Qt::ImhDate | Qt::ImhTime; |
| break; |
| case content_purpose_terminal: |
| default: |
| break; |
| } |
| |
| pendingState->changedState |= Qt::ImHints; |
| } |
| |
| void QWaylandTextInputPrivate::zwp_text_input_v2_set_preferred_language(Resource *resource, const QString &language) |
| { |
| if (resource != focusResource) |
| return; |
| |
| pendingState->preferredLanguage = language; |
| |
| pendingState->changedState |= Qt::ImPreferredLanguage; |
| } |
| |
| void QWaylandTextInputPrivate::zwp_text_input_v2_set_surrounding_text(Resource *resource, const QString &text, int32_t cursor, int32_t anchor) |
| { |
| if (resource != focusResource) |
| return; |
| |
| pendingState->surroundingText = text; |
| pendingState->cursorPosition = QWaylandInputMethodEventBuilder::indexFromWayland(text, cursor); |
| pendingState->anchorPosition = QWaylandInputMethodEventBuilder::indexFromWayland(text, anchor); |
| |
| pendingState->changedState |= Qt::ImSurroundingText | Qt::ImCursorPosition | Qt::ImAnchorPosition; |
| } |
| |
| QWaylandTextInput::QWaylandTextInput(QWaylandObject *container, QWaylandCompositor *compositor) |
| : QWaylandCompositorExtensionTemplate(container, *new QWaylandTextInputPrivate(compositor)) |
| { |
| connect(&d_func()->focusDestroyListener, &QWaylandDestroyListener::fired, |
| this, &QWaylandTextInput::focusSurfaceDestroyed); |
| |
| connect(qApp->inputMethod(), &QInputMethod::visibleChanged, |
| this, &QWaylandTextInput::sendInputPanelState); |
| connect(qApp->inputMethod(), &QInputMethod::keyboardRectangleChanged, |
| this, &QWaylandTextInput::sendInputPanelState); |
| connect(qApp->inputMethod(), &QInputMethod::inputDirectionChanged, |
| this, &QWaylandTextInput::sendTextDirection); |
| connect(qApp->inputMethod(), &QInputMethod::localeChanged, |
| this, &QWaylandTextInput::sendLocale); |
| } |
| |
| QWaylandTextInput::~QWaylandTextInput() |
| { |
| } |
| |
| void QWaylandTextInput::sendInputMethodEvent(QInputMethodEvent *event) |
| { |
| Q_D(QWaylandTextInput); |
| |
| d->sendInputMethodEvent(event); |
| } |
| |
| void QWaylandTextInput::sendKeyEvent(QKeyEvent *event) |
| { |
| Q_D(QWaylandTextInput); |
| |
| d->sendKeyEvent(event); |
| } |
| |
| void QWaylandTextInput::sendInputPanelState() |
| { |
| Q_D(QWaylandTextInput); |
| |
| d->sendInputPanelState(); |
| } |
| |
| void QWaylandTextInput::sendTextDirection() |
| { |
| Q_D(QWaylandTextInput); |
| |
| d->sendTextDirection(); |
| } |
| |
| void QWaylandTextInput::sendLocale() |
| { |
| Q_D(QWaylandTextInput); |
| |
| d->sendLocale(); |
| } |
| |
| QVariant QWaylandTextInput::inputMethodQuery(Qt::InputMethodQuery property, QVariant argument) const |
| { |
| const Q_D(QWaylandTextInput); |
| |
| return d->inputMethodQuery(property, argument); |
| } |
| |
| QWaylandSurface *QWaylandTextInput::focus() const |
| { |
| const Q_D(QWaylandTextInput); |
| |
| return d->focus; |
| } |
| |
| void QWaylandTextInput::setFocus(QWaylandSurface *surface) |
| { |
| Q_D(QWaylandTextInput); |
| |
| d->setFocus(surface); |
| } |
| |
| void QWaylandTextInput::focusSurfaceDestroyed(void *) |
| { |
| Q_D(QWaylandTextInput); |
| |
| d->focusDestroyListener.reset(); |
| |
| d->focus = nullptr; |
| d->focusResource = nullptr; |
| } |
| |
| bool QWaylandTextInput::isSurfaceEnabled(QWaylandSurface *surface) const |
| { |
| const Q_D(QWaylandTextInput); |
| |
| return d->enabledSurfaces.values().contains(surface); |
| } |
| |
| void QWaylandTextInput::add(::wl_client *client, uint32_t id, int version) |
| { |
| Q_D(QWaylandTextInput); |
| |
| d->add(client, id, version); |
| } |
| |
| const wl_interface *QWaylandTextInput::interface() |
| { |
| return QWaylandTextInputPrivate::interface(); |
| } |
| |
| QByteArray QWaylandTextInput::interfaceName() |
| { |
| return QWaylandTextInputPrivate::interfaceName(); |
| } |
| |
| QT_END_NAMESPACE |