You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1586 lines
62 KiB
C++
1586 lines
62 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
#include "UiTextInputComponent.h"
|
|
|
|
#include <AzCore/Math/Crc.h>
|
|
#include <AzCore/Serialization/SerializeContext.h>
|
|
#include <AzCore/Serialization/EditContext.h>
|
|
#include <AzCore/std/string/conversions.h>
|
|
#include <AzCore/RTTI/BehaviorContext.h>
|
|
#include <AzCore/Component/ComponentApplicationBus.h>
|
|
|
|
#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
|
|
|
|
#include <IRenderer.h>
|
|
#include <ITimer.h>
|
|
#include <LyShine/Bus/UiElementBus.h>
|
|
#include <LyShine/Bus/UiTransformBus.h>
|
|
#include <LyShine/Bus/UiVisualBus.h>
|
|
#include <LyShine/Bus/UiCanvasBus.h>
|
|
#include <LyShine/Bus/UiTextBus.h>
|
|
#include <LyShine/ISprite.h>
|
|
|
|
#include <LyShine/IDraw2d.h>
|
|
#include <LyShine/UiSerializeHelpers.h>
|
|
|
|
|
|
#include "UiNavigationHelpers.h"
|
|
#include "UiSerialize.h"
|
|
#include "Sprite.h"
|
|
#include "StringUtfUtils.h"
|
|
#include "UiClipboard.h"
|
|
|
|
namespace
|
|
{
|
|
// Orange color from the canvas editor style guide
|
|
const AZ::Color defaultSelectionColor(255.0f / 255.0f, 153.0f / 255.0f, 0.0f / 255.0f, 1.0f);
|
|
// White color from the canvas editor style guide
|
|
const AZ::Color defaultCursorColor(238.0f / 255.0f, 238.0f / 255.0f, 238.0f / 255.0f, 1.0f);
|
|
|
|
const uint32_t defaultReplacementChar('*');
|
|
|
|
// Add all descendant elements that support the UiTextBus to a list of pairs of
|
|
// entity ID and string.
|
|
void AddDescendantTextElements(AZ::EntityId entity,
|
|
UiTextInputComponent::EntityComboBoxVec& result)
|
|
{
|
|
// Get a list of all descendant elements that support the UiTextBus
|
|
LyShine::EntityArray matchingElements;
|
|
EBUS_EVENT_ID(entity, UiElementBus, FindDescendantElements,
|
|
[](const AZ::Entity* descendant) { return UiTextBus::FindFirstHandler(descendant->GetId()) != nullptr; },
|
|
matchingElements);
|
|
|
|
// add their names to the StringList and their IDs to the id list
|
|
for (auto childEntity : matchingElements)
|
|
{
|
|
result.push_back(AZStd::make_pair(AZ::EntityId(childEntity->GetId()), childEntity->GetName()));
|
|
}
|
|
}
|
|
|
|
//! \brief Given a UTF8 string and index, return the raw string buffer index that maps to the UTF8 index.
|
|
int GetCharArrayIndexFromUtf8CharIndex(const AZStd::string& utf8String, const uint utf8Index)
|
|
{
|
|
uint utfIndexIter = 0;
|
|
int rawIndex = 0;
|
|
|
|
const AZStd::string::size_type stringLength = utf8String.length();
|
|
if (stringLength > 0 && stringLength >= utf8Index)
|
|
{
|
|
// Iterate over the string until the given index is found.
|
|
Utf8::Unchecked::octet_iterator pChar(utf8String.data());
|
|
while (uint32_t ch = *pChar)
|
|
{
|
|
if (utf8Index == utfIndexIter)
|
|
{
|
|
break;
|
|
}
|
|
++utfIndexIter;
|
|
|
|
// Add up the size of the multibyte chars along the way,
|
|
// which will give us the "raw" string buffer index of where
|
|
// the given index maps to.
|
|
rawIndex += LyShine::GetMultiByteCharSize(ch);
|
|
|
|
++pChar;
|
|
}
|
|
}
|
|
|
|
return rawIndex;
|
|
}
|
|
|
|
//! \brief Removes a range of UTF8 code points using the given indices.
|
|
//! The given indices are code-point indices and not raw (byte) indices.
|
|
void RemoveUtf8CodePointsByIndex(AZStd::string& utf8String, int index1, int index2)
|
|
{
|
|
const int minSelectIndex = min(index1, index2);
|
|
const int maxSelectIndex = max(index1, index2);
|
|
const int left = GetCharArrayIndexFromUtf8CharIndex(utf8String, minSelectIndex);
|
|
const int right = GetCharArrayIndexFromUtf8CharIndex(utf8String, maxSelectIndex);
|
|
utf8String.erase(left, right - left);
|
|
}
|
|
|
|
//! \brief Returns a UTF8 sub-string using the given indices.
|
|
//! The given indices are code-point indices and not raw (byte) indices.
|
|
AZStd::string Utf8SubString(const AZStd::string& utf8String, int utf8CharIndexStart, int utf8CharIndexEnd)
|
|
{
|
|
const int minCharIndex = min(utf8CharIndexStart, utf8CharIndexEnd);
|
|
const int maxCharIndex = max(utf8CharIndexStart, utf8CharIndexEnd);
|
|
const int left = GetCharArrayIndexFromUtf8CharIndex(utf8String, minCharIndex);
|
|
const int right = GetCharArrayIndexFromUtf8CharIndex(utf8String, maxCharIndex);
|
|
return utf8String.substr(left, right - left);
|
|
}
|
|
|
|
//! \brief Convenience method for erasing a range of text and updating the given selection indices accordingly.
|
|
void EraseAndUpdateSelectionRange(AZStd::string& utf8String, int& endSelectIndex, int& startSelectIndex)
|
|
{
|
|
RemoveUtf8CodePointsByIndex(utf8String, endSelectIndex, startSelectIndex);
|
|
endSelectIndex = startSelectIndex = min(endSelectIndex, startSelectIndex);
|
|
}
|
|
} // anonymous namespace
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//! UiTextInputNotificationBus Behavior context handler class
|
|
class BehaviorUiTextInputNotificationBusHandler
|
|
: public UiTextInputNotificationBus::Handler
|
|
, public AZ::BehaviorEBusHandler
|
|
{
|
|
public:
|
|
AZ_EBUS_BEHAVIOR_BINDER(BehaviorUiTextInputNotificationBusHandler, "{5ED20B32-95E2-4EBB-8874-7E780306F7F0}", AZ::SystemAllocator,
|
|
OnTextInputChange, OnTextInputEndEdit, OnTextInputEnter);
|
|
|
|
void OnTextInputChange(const AZStd::string& textString) override
|
|
{
|
|
Call(FN_OnTextInputChange, textString);
|
|
}
|
|
|
|
void OnTextInputEndEdit(const AZStd::string& textString) override
|
|
{
|
|
Call(FN_OnTextInputEndEdit, textString);
|
|
}
|
|
|
|
void OnTextInputEnter(const AZStd::string& textString) override
|
|
{
|
|
Call(FN_OnTextInputEnter, textString);
|
|
}
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// PUBLIC MEMBER FUNCTIONS
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiTextInputComponent::UiTextInputComponent()
|
|
: m_isDragging(false)
|
|
, m_isEditing(false)
|
|
, m_isTextInputStarted(false)
|
|
, m_textCursorPos(-1)
|
|
, m_textSelectionStartPos(-1)
|
|
, m_cursorBlinkStartTime(0.0f)
|
|
, m_textEntity()
|
|
, m_placeHolderTextEntity()
|
|
, m_textSelectionColor(defaultSelectionColor)
|
|
, m_textCursorColor(defaultCursorColor)
|
|
, m_maxStringLength(-1)
|
|
, m_cursorBlinkInterval(1.0f)
|
|
, m_childTextStateDirtyFlag(true)
|
|
, m_onChange(nullptr)
|
|
, m_onEndEdit(nullptr)
|
|
, m_onEnter(nullptr)
|
|
, m_replacementCharacter(defaultReplacementChar)
|
|
, m_isPasswordField(false)
|
|
, m_clipInputText(true)
|
|
, m_enableClipboard(true)
|
|
{
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiTextInputComponent::~UiTextInputComponent()
|
|
{
|
|
if (m_isEditing)
|
|
{
|
|
AzFramework::InputTextEntryRequestBus::Broadcast(&AzFramework::InputTextEntryRequests::TextEntryStop);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiTextInputComponent::HandlePressed(AZ::Vector2 point, bool& shouldStayActive)
|
|
{
|
|
bool handled = UiInteractableComponent::HandlePressed(point, shouldStayActive);
|
|
|
|
if (handled)
|
|
{
|
|
// clear the dragging flag, we are not dragging until we detect a drag
|
|
m_isDragging = false;
|
|
|
|
// the text input field will stay active after released
|
|
shouldStayActive = true;
|
|
|
|
// store the character position where the press corresponds to in the text string
|
|
EBUS_EVENT_ID_RESULT(m_textCursorPos, m_textEntity, UiTextBus, GetCharIndexFromPoint, point, false);
|
|
m_textSelectionStartPos = m_textCursorPos;
|
|
}
|
|
|
|
ResetCursorBlink();
|
|
|
|
return handled;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiTextInputComponent::HandleReleased(AZ::Vector2 point)
|
|
{
|
|
m_isPressed = false;
|
|
m_isDragging = false;
|
|
|
|
if (!m_isHandlingEvents)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!m_isEditing)
|
|
{
|
|
bool isInRect = false;
|
|
EBUS_EVENT_ID_RESULT(isInRect, GetEntityId(), UiTransformBus, IsPointInRect, point);
|
|
if (isInRect)
|
|
{
|
|
BeginEditState();
|
|
}
|
|
else
|
|
{
|
|
// cancel the active status
|
|
EBUS_EVENT_ID(GetEntityId(), UiInteractableActiveNotificationBus, ActiveCancelled);
|
|
}
|
|
}
|
|
|
|
CheckStartTextInput();
|
|
|
|
UiInteractableComponent::TriggerReleasedAction();
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiTextInputComponent::HandleEnterPressed(bool& shouldStayActive)
|
|
{
|
|
bool handled = UiInteractableComponent::HandleEnterPressed(shouldStayActive);
|
|
|
|
if (handled)
|
|
{
|
|
// the text input field will stay active after released
|
|
shouldStayActive = true;
|
|
|
|
AZStd::string textString;
|
|
EBUS_EVENT_ID_RESULT(textString, m_textEntity, UiTextBus, GetText);
|
|
|
|
// select all the text
|
|
m_textCursorPos = 0;
|
|
m_textSelectionStartPos = LyShine::GetUtf8StringLength(textString);
|
|
}
|
|
|
|
return handled;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiTextInputComponent::HandleEnterReleased()
|
|
{
|
|
m_isPressed = false;
|
|
|
|
if (!m_isHandlingEvents)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!m_isEditing)
|
|
{
|
|
BeginEditState();
|
|
}
|
|
|
|
CheckStartTextInput();
|
|
|
|
UiInteractableComponent::TriggerReleasedAction();
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiTextInputComponent::HandleAutoActivation()
|
|
{
|
|
if (!m_isHandlingEvents)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AZStd::string textString;
|
|
EBUS_EVENT_ID_RESULT(textString, m_textEntity, UiTextBus, GetText);
|
|
m_textCursorPos = LyShine::GetUtf8StringLength(textString);
|
|
m_textSelectionStartPos = m_textCursorPos;
|
|
|
|
if (!m_isEditing)
|
|
{
|
|
BeginEditState();
|
|
}
|
|
|
|
CheckStartTextInput();
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiTextInputComponent::HandleTextInput(const AZStd::string& inputTextUTF8)
|
|
{
|
|
if (!m_isHandlingEvents)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// don't accept text input while in pressed state
|
|
if (m_isPressed)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AZStd::string currentText;
|
|
EBUS_EVENT_ID_RESULT(currentText, m_textEntity, UiTextBus, GetText);
|
|
|
|
bool changedText = false;
|
|
|
|
if (inputTextUTF8 == "\b" || inputTextUTF8 == "\x7f")
|
|
{
|
|
// backspace pressed, delete character before cursor or the selected range
|
|
if (m_textCursorPos > 0 || m_textCursorPos != m_textSelectionStartPos)
|
|
{
|
|
if (m_textCursorPos != m_textSelectionStartPos)
|
|
{
|
|
// range is selected
|
|
EraseAndUpdateSelectionRange(currentText, m_textCursorPos, m_textSelectionStartPos);
|
|
}
|
|
else
|
|
{
|
|
// "Select" one codepoint to erase (via backspace)
|
|
m_textSelectionStartPos = m_textCursorPos - 1;
|
|
EraseAndUpdateSelectionRange(currentText, m_textCursorPos, m_textSelectionStartPos);
|
|
}
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, m_textCursorColor);
|
|
|
|
changedText = true;
|
|
}
|
|
}
|
|
// if inputTextUTF8 is a control character (a non printing character such as esc or tab) ignore it
|
|
else if (inputTextUTF8.size() != 1 || !AZStd::is_cntrl(inputTextUTF8.at(0)))
|
|
{
|
|
// note currently we are treating the wchar passed in as a char, for localization
|
|
// we need to use a wide string or utf8 string
|
|
if (m_textCursorPos >= 0)
|
|
{
|
|
// if a range is selected then erase that first
|
|
if (m_textCursorPos != m_textSelectionStartPos)
|
|
{
|
|
EraseAndUpdateSelectionRange(currentText, m_textCursorPos, m_textSelectionStartPos);
|
|
changedText = true;
|
|
}
|
|
|
|
// only allow text to be added if there is no length limit or the length is under the limit
|
|
if (m_maxStringLength < 0 || currentText.length() < m_maxStringLength)
|
|
{
|
|
int rawIndexPos = GetCharArrayIndexFromUtf8CharIndex(currentText, m_textCursorPos);
|
|
|
|
if (rawIndexPos >= 0)
|
|
{
|
|
currentText.insert(rawIndexPos, inputTextUTF8);
|
|
|
|
m_textCursorPos++;
|
|
m_textSelectionStartPos = m_textCursorPos;
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, m_textCursorColor);
|
|
changedText = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (changedText)
|
|
{
|
|
ChangeText(currentText);
|
|
ResetCursorBlink();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiTextInputComponent::HandleKeyInputBegan(const AzFramework::InputChannel::Snapshot& inputSnapshot, AzFramework::ModifierKeyMask activeModifierKeys)
|
|
{
|
|
if (!m_isHandlingEvents)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// don't accept character input while in pressed state
|
|
if (m_isPressed)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool result = true;
|
|
|
|
int oldTextCursorPos = m_textCursorPos;
|
|
int oldTextSelectionStartPos = m_textSelectionStartPos;
|
|
|
|
const bool isShiftModifierActive = (static_cast<int>(activeModifierKeys) & static_cast<int>(AzFramework::ModifierKeyMask::ShiftAny)) != 0;
|
|
const bool isLCTRLModifierActive = (static_cast<int>(activeModifierKeys) & static_cast<int>(AzFramework::ModifierKeyMask::CtrlAny)) != 0;
|
|
const UiNavigationHelpers::Command command = UiNavigationHelpers::MapInputChannelIdToUiNavigationCommand(inputSnapshot.m_channelId, activeModifierKeys);
|
|
if (command == UiNavigationHelpers::Command::Enter)
|
|
{
|
|
// enter was pressed
|
|
|
|
AZStd::string textString;
|
|
EBUS_EVENT_ID_RESULT(textString, m_textEntity, UiTextBus, GetText);
|
|
|
|
// if a C++ callback is registered for OnEnter then call it
|
|
if (m_onEnter)
|
|
{
|
|
// pass the entered text string to the C++ callback
|
|
m_onEnter(GetEntityId(), textString);
|
|
}
|
|
|
|
// Tell any action listeners about the event
|
|
if (!m_enterAction.empty())
|
|
{
|
|
// canvas listeners will get the action name (e.g. something like "EmailEntered") plus
|
|
// the ID of this entity.
|
|
AZ::EntityId canvasEntityId;
|
|
EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId);
|
|
EBUS_EVENT_ID(canvasEntityId, UiCanvasNotificationBus, OnAction, GetEntityId(), m_enterAction);
|
|
}
|
|
|
|
EBUS_EVENT_ID(GetEntityId(), UiTextInputNotificationBus, OnTextInputEnter, textString);
|
|
|
|
// cancel the active status
|
|
EBUS_EVENT_ID(GetEntityId(), UiInteractableActiveNotificationBus, ActiveCancelled);
|
|
EndEditState();
|
|
}
|
|
else if (inputSnapshot.m_channelId == AzFramework::InputDeviceKeyboard::Key::NavigationDelete)
|
|
{
|
|
AZStd::string textString;
|
|
EBUS_EVENT_ID_RESULT(textString, m_textEntity, UiTextBus, GetText);
|
|
|
|
// Delete pressed, delete character after cursor or the selected range
|
|
if (m_textCursorPos < LyShine::GetUtf8StringLength(textString) || m_textCursorPos != m_textSelectionStartPos)
|
|
{
|
|
if (m_textCursorPos != m_textSelectionStartPos)
|
|
{
|
|
// range is selected
|
|
EraseAndUpdateSelectionRange(textString, m_textCursorPos, m_textSelectionStartPos);
|
|
}
|
|
else
|
|
{
|
|
// no range selected - delete character after cursor
|
|
RemoveUtf8CodePointsByIndex(textString, m_textCursorPos, m_textCursorPos + 1);
|
|
}
|
|
|
|
ChangeText(textString);
|
|
}
|
|
}
|
|
else if (command == UiNavigationHelpers::Command::Left || command == UiNavigationHelpers::Command::Right)
|
|
{
|
|
if (m_textCursorPos != m_textSelectionStartPos)
|
|
{
|
|
// Range is selected
|
|
if (isShiftModifierActive)
|
|
{
|
|
// Move cursor to change selected range
|
|
AZStd::string textString;
|
|
EBUS_EVENT_ID_RESULT(textString, m_textEntity, UiTextBus, GetText);
|
|
|
|
if (command == UiNavigationHelpers::Command::Left)
|
|
{
|
|
if (m_textCursorPos > 0)
|
|
{
|
|
--m_textCursorPos;
|
|
}
|
|
}
|
|
else // UiNavigationHelpers::Command::Right
|
|
{
|
|
if (m_textCursorPos < LyShine::GetUtf8StringLength(textString))
|
|
{
|
|
++m_textCursorPos;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Place cursor at start or end of selection
|
|
if (command == UiNavigationHelpers::Command::Left)
|
|
{
|
|
m_textCursorPos = min(m_textCursorPos, m_textSelectionStartPos);
|
|
}
|
|
else // eKI_Right
|
|
{
|
|
m_textCursorPos = max(m_textCursorPos, m_textSelectionStartPos);
|
|
}
|
|
m_textSelectionStartPos = m_textCursorPos;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No range selected, move cursor one character
|
|
AZStd::string textString;
|
|
EBUS_EVENT_ID_RESULT(textString, m_textEntity, UiTextBus, GetText);
|
|
|
|
if (command == UiNavigationHelpers::Command::Left)
|
|
{
|
|
if (m_textCursorPos > 0)
|
|
{
|
|
--m_textCursorPos;
|
|
}
|
|
}
|
|
else // eKI_Right
|
|
{
|
|
if (m_textCursorPos < LyShine::GetUtf8StringLength(textString))
|
|
{
|
|
++m_textCursorPos;
|
|
}
|
|
}
|
|
|
|
if (!isShiftModifierActive)
|
|
{
|
|
m_textSelectionStartPos = m_textCursorPos;
|
|
}
|
|
}
|
|
}
|
|
else if (command == UiNavigationHelpers::Command::Up || command == UiNavigationHelpers::Command::Down)
|
|
{
|
|
AZ::Vector2 currentPosition;
|
|
EBUS_EVENT_ID_RESULT(currentPosition, m_textEntity, UiTextBus, GetPointFromCharIndex, m_textCursorPos);
|
|
|
|
float fontSize;
|
|
EBUS_EVENT_ID_RESULT(fontSize, m_textEntity, UiTextBus, GetFontSize);
|
|
|
|
// To get the position of the cursor on the line above or below the
|
|
// current cursor position, we add or subtract the font size,
|
|
// depending on whether arrow key up or down is provided.
|
|
if (command == UiNavigationHelpers::Command::Up)
|
|
{
|
|
fontSize *= -1.0f;
|
|
}
|
|
|
|
// Get the index that matches closest to the position directly above
|
|
// or below the current cursor position.
|
|
currentPosition.SetY(currentPosition.GetY() + fontSize);
|
|
int adjustedIndex = 0;
|
|
EBUS_EVENT_ID_RESULT(adjustedIndex, m_textEntity, UiTextBus, GetCharIndexFromCanvasSpacePoint, currentPosition, true);
|
|
|
|
if (adjustedIndex != -1)
|
|
{
|
|
if (isShiftModifierActive)
|
|
{
|
|
m_textCursorPos = adjustedIndex;
|
|
}
|
|
else
|
|
{
|
|
result = m_textCursorPos != adjustedIndex;
|
|
m_textCursorPos = m_textSelectionStartPos = adjustedIndex;
|
|
}
|
|
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, m_textCursorColor);
|
|
}
|
|
else
|
|
{
|
|
result = isShiftModifierActive;
|
|
}
|
|
}
|
|
else if ((inputSnapshot.m_channelId == AzFramework::InputDeviceKeyboard::Key::AlphanumericA) &&
|
|
(static_cast<int>(activeModifierKeys) & static_cast<int>(AzFramework::ModifierKeyMask::CtrlAny)))
|
|
{
|
|
// Select all
|
|
AZStd::string textString;
|
|
EBUS_EVENT_ID_RESULT(textString, m_textEntity, UiTextBus, GetText);
|
|
|
|
m_textSelectionStartPos = 0;
|
|
m_textCursorPos = LyShine::GetUtf8StringLength(textString);
|
|
}
|
|
else if (command == UiNavigationHelpers::Command::NavHome)
|
|
{
|
|
// Move cursor to start of text
|
|
m_textCursorPos = 0;
|
|
if (!isShiftModifierActive)
|
|
{
|
|
m_textSelectionStartPos = m_textCursorPos;
|
|
}
|
|
}
|
|
else if (command == UiNavigationHelpers::Command::NavEnd)
|
|
{
|
|
// Move cursor to end of text
|
|
AZStd::string textString;
|
|
EBUS_EVENT_ID_RESULT(textString, m_textEntity, UiTextBus, GetText);
|
|
|
|
m_textCursorPos = LyShine::GetUtf8StringLength(textString);
|
|
if (!isShiftModifierActive)
|
|
{
|
|
m_textSelectionStartPos = m_textCursorPos;
|
|
}
|
|
}
|
|
else if (m_enableClipboard && (inputSnapshot.m_channelId == AzFramework::InputDeviceKeyboard::Key::AlphanumericC) && isLCTRLModifierActive)
|
|
{
|
|
AZStd::string textString;
|
|
EBUS_EVENT_ID_RESULT(textString, m_textEntity, UiTextBus, GetText);
|
|
if (textString.length() > 0 && m_textCursorPos != m_textSelectionStartPos)
|
|
{
|
|
int left = min(m_textCursorPos, m_textSelectionStartPos);
|
|
int right = max(m_textCursorPos, m_textSelectionStartPos);
|
|
UiClipboard::SetText(textString.substr(left, right - left));
|
|
}
|
|
}
|
|
else if (m_enableClipboard && (inputSnapshot.m_channelId == AzFramework::InputDeviceKeyboard::Key::AlphanumericX) && isLCTRLModifierActive)
|
|
{
|
|
AZStd::string textString;
|
|
EBUS_EVENT_ID_RESULT(textString, m_textEntity, UiTextBus, GetText);
|
|
if (textString.length() > 0 && m_textCursorPos != m_textSelectionStartPos)
|
|
{
|
|
int left = min(m_textCursorPos, m_textSelectionStartPos);
|
|
int right = max(m_textCursorPos, m_textSelectionStartPos);
|
|
UiClipboard::SetText(textString.substr(left, right - left));
|
|
textString.erase(left, right - left);
|
|
m_textCursorPos = m_textSelectionStartPos = left;
|
|
|
|
ChangeText(textString);
|
|
ResetCursorBlink();
|
|
}
|
|
}
|
|
else if (m_enableClipboard && (inputSnapshot.m_channelId == AzFramework::InputDeviceKeyboard::Key::AlphanumericV) && isLCTRLModifierActive)
|
|
{
|
|
auto clipboardText = UiClipboard::GetText();
|
|
if (clipboardText.length() > 0)
|
|
{
|
|
AZStd::string textString;
|
|
EBUS_EVENT_ID_RESULT(textString, m_textEntity, UiTextBus, GetText);
|
|
|
|
// If a range is selected then erase that first
|
|
if (m_textCursorPos != m_textSelectionStartPos)
|
|
{
|
|
int left = min(m_textCursorPos, m_textSelectionStartPos);
|
|
int right = max(m_textCursorPos, m_textSelectionStartPos);
|
|
textString.erase(left, right - left);
|
|
m_textCursorPos = m_textSelectionStartPos = left;
|
|
}
|
|
|
|
// Append text from clipboard
|
|
textString.insert(m_textCursorPos, clipboardText);
|
|
m_textCursorPos += static_cast<int>(clipboardText.length());
|
|
m_textSelectionStartPos = m_textCursorPos;
|
|
|
|
// If max length is set, remove extra characters
|
|
if (m_maxStringLength >= 0 && textString.length() > m_maxStringLength)
|
|
{
|
|
textString.resize(m_maxStringLength);
|
|
}
|
|
|
|
ChangeText(textString);
|
|
ResetCursorBlink();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = false;
|
|
}
|
|
|
|
if (m_textCursorPos != oldTextCursorPos || m_textSelectionStartPos != oldTextSelectionStartPos)
|
|
{
|
|
AZ::Color color = (m_textSelectionStartPos == m_textCursorPos) ? m_textCursorColor : m_textSelectionColor;
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, color);
|
|
if (m_textSelectionStartPos == m_textCursorPos)
|
|
{
|
|
ResetCursorBlink();
|
|
}
|
|
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, ResetCursorLineHint);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::InputPositionUpdate(AZ::Vector2 point)
|
|
{
|
|
// support dragging to select text, but also support being in a parent draggable
|
|
if (m_isPressed)
|
|
{
|
|
// if we are not yet in the dragging state do some tests to see if we should be
|
|
if (!m_isDragging)
|
|
{
|
|
CheckForDragOrHandOffToParent(point);
|
|
}
|
|
|
|
if (m_isDragging)
|
|
{
|
|
EBUS_EVENT_ID_RESULT(m_textCursorPos, m_textEntity, UiTextBus, GetCharIndexFromPoint, point, false);
|
|
AZ::Color color = (m_textSelectionStartPos == m_textCursorPos) ? m_textCursorColor : m_textSelectionColor;
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, color);
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::LostActiveStatus()
|
|
{
|
|
UiInteractableComponent::LostActiveStatus();
|
|
|
|
EndEditState();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::Update(float deltaTime)
|
|
{
|
|
UiInteractableComponent::Update(deltaTime);
|
|
|
|
// if we have not set the enable/disable status of the text and placeholder text since
|
|
// our status changed then set it
|
|
if (m_childTextStateDirtyFlag)
|
|
{
|
|
bool displayPlaceHolder = true;
|
|
|
|
if (m_isEditing)
|
|
{
|
|
displayPlaceHolder = false;
|
|
}
|
|
else
|
|
{
|
|
AZStd::string text;
|
|
EBUS_EVENT_ID_RESULT(text, m_textEntity, UiTextBus, GetText);
|
|
if (!text.empty())
|
|
{
|
|
displayPlaceHolder = false;
|
|
}
|
|
}
|
|
|
|
EBUS_EVENT_ID(m_placeHolderTextEntity, UiElementBus, SetIsEnabled, displayPlaceHolder);
|
|
EBUS_EVENT_ID(m_textEntity, UiElementBus, SetIsEnabled, !displayPlaceHolder);
|
|
|
|
m_childTextStateDirtyFlag = false;
|
|
}
|
|
|
|
// update cursor blinking, only if: this component is active, and blink interval set, and there is no text selection
|
|
if (m_isEditing && m_cursorBlinkInterval > 0.0f && m_textSelectionStartPos == m_textCursorPos)
|
|
{
|
|
if (m_cursorBlinkStartTime == 0.0f)
|
|
{
|
|
m_cursorBlinkStartTime = gEnv->pTimer->GetCurrTime(ITimer::ETIMER_UI);
|
|
}
|
|
else
|
|
{
|
|
const float currentTime = gEnv->pTimer->GetCurrTime(ITimer::ETIMER_UI);
|
|
if (currentTime - m_cursorBlinkStartTime > m_cursorBlinkInterval * 0.5f)
|
|
{
|
|
m_textCursorColor.SetA(m_textCursorColor.GetA() ? 0.0f : 1.0f);
|
|
m_cursorBlinkStartTime = currentTime;
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, m_textCursorColor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::InGamePostActivate()
|
|
{
|
|
UpdateDisplayedTextFunction();
|
|
|
|
if (m_clipInputText)
|
|
{
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, SetOverflowMode, UiTextInterface::OverflowMode::ClipText);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiTextInputComponent::GetIsPasswordField()
|
|
{
|
|
return m_isPasswordField;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetIsPasswordField(bool passwordField)
|
|
{
|
|
m_isPasswordField = passwordField;
|
|
UpdateDisplayedTextFunction();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
uint32_t UiTextInputComponent::GetReplacementCharacter()
|
|
{
|
|
// We store our replacement character as a string due to a reflection issue
|
|
// with chars in the editor, so as a workaround we only deal with the first
|
|
// character of the string.
|
|
return m_replacementCharacter;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetReplacementCharacter(uint32_t replacementChar)
|
|
{
|
|
m_replacementCharacter = replacementChar;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::Color UiTextInputComponent::GetTextSelectionColor()
|
|
{
|
|
return m_textSelectionColor;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetTextSelectionColor(const AZ::Color& color)
|
|
{
|
|
m_textSelectionColor = color;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::Color UiTextInputComponent::GetTextCursorColor()
|
|
{
|
|
return m_textCursorColor;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetTextCursorColor(const AZ::Color& color)
|
|
{
|
|
m_textCursorColor = color;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
float UiTextInputComponent::GetCursorBlinkInterval()
|
|
{
|
|
return m_cursorBlinkInterval;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetCursorBlinkInterval(float interval)
|
|
{
|
|
m_cursorBlinkInterval = interval;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
int UiTextInputComponent::GetMaxStringLength()
|
|
{
|
|
return m_maxStringLength;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetMaxStringLength(int maxCharacters)
|
|
{
|
|
m_maxStringLength = maxCharacters;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiTextInputComponent::TextInputCallback UiTextInputComponent::GetOnChangeCallback()
|
|
{
|
|
return m_onChange;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetOnChangeCallback(TextInputCallback callbackFunction)
|
|
{
|
|
m_onChange = callbackFunction;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiTextInputComponent::TextInputCallback UiTextInputComponent::GetOnEndEditCallback()
|
|
{
|
|
return m_onEndEdit;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetOnEndEditCallback(TextInputCallback callbackFunction)
|
|
{
|
|
m_onEndEdit = callbackFunction;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiTextInputComponent::TextInputCallback UiTextInputComponent::GetOnEnterCallback()
|
|
{
|
|
return m_onEnter;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetOnEnterCallback(TextInputCallback callbackFunction)
|
|
{
|
|
m_onEnter = callbackFunction;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
const LyShine::ActionName& UiTextInputComponent::GetChangeAction()
|
|
{
|
|
return m_changeAction;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetChangeAction(const LyShine::ActionName& actionName)
|
|
{
|
|
m_changeAction = actionName;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
const LyShine::ActionName& UiTextInputComponent::GetEndEditAction()
|
|
{
|
|
return m_endEditAction;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetEndEditAction(const LyShine::ActionName& actionName)
|
|
{
|
|
m_endEditAction = actionName;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
const LyShine::ActionName& UiTextInputComponent::GetEnterAction()
|
|
{
|
|
return m_enterAction;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetEnterAction(const LyShine::ActionName& actionName)
|
|
{
|
|
m_enterAction = actionName;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiTextInputComponent::GetTextEntity()
|
|
{
|
|
return m_textEntity;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetTextEntity(AZ::EntityId textEntity)
|
|
{
|
|
m_textEntity = textEntity;
|
|
m_childTextStateDirtyFlag = true;
|
|
UpdateDisplayedTextFunction();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZStd::string UiTextInputComponent::GetText()
|
|
{
|
|
AZStd::string text;
|
|
EBUS_EVENT_ID_RESULT(text, m_textEntity, UiTextBus, GetText);
|
|
return text;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetText(const AZStd::string& text)
|
|
{
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, SetText, text);
|
|
m_childTextStateDirtyFlag = true;
|
|
|
|
// Make sure cursor position and selection is in range
|
|
if (m_textCursorPos >= 0)
|
|
{
|
|
int maxPos = LyShine::GetUtf8StringLength(text);
|
|
int newTextCursorPos = AZ::GetMin(m_textCursorPos, maxPos);
|
|
int newTextSelectionStartPos = AZ::GetMin(m_textSelectionStartPos, maxPos);
|
|
|
|
if (newTextCursorPos != m_textCursorPos || newTextSelectionStartPos != m_textSelectionStartPos)
|
|
{
|
|
m_textCursorPos = newTextCursorPos;
|
|
m_textSelectionStartPos = newTextSelectionStartPos;
|
|
|
|
int selStartIndex, selEndIndex;
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, GetSelectionRange, selStartIndex, selEndIndex);
|
|
if (selStartIndex >= 0)
|
|
{
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, m_textCursorColor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiTextInputComponent::GetPlaceHolderTextEntity()
|
|
{
|
|
return m_placeHolderTextEntity;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetPlaceHolderTextEntity(AZ::EntityId textEntity)
|
|
{
|
|
m_placeHolderTextEntity = textEntity;
|
|
m_childTextStateDirtyFlag = true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiTextInputComponent::GetIsClipboardEnabled()
|
|
{
|
|
return m_enableClipboard;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::SetIsClipboardEnabled(bool enableClipboard)
|
|
{
|
|
m_enableClipboard = enableClipboard;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// PROTECTED MEMBER FUNCTIONS
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::Activate()
|
|
{
|
|
UiInteractableComponent::Activate();
|
|
UiInitializationBus::Handler::BusConnect(m_entity->GetId());
|
|
UiTextInputBus::Handler::BusConnect(m_entity->GetId());
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::Deactivate()
|
|
{
|
|
UiInteractableComponent::Deactivate();
|
|
UiInitializationBus::Handler::BusDisconnect();
|
|
UiTextInputBus::Handler::BusDisconnect();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiTextInputComponent::IsAutoActivationSupported()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::BeginEditState()
|
|
{
|
|
m_isEditing = true;
|
|
|
|
// force re-evaluation of whether text or placeholder text should be displayed
|
|
m_childTextStateDirtyFlag = true;
|
|
|
|
// position the cursor in the text entity
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, m_textCursorColor);
|
|
|
|
ResetCursorBlink();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::EndEditState()
|
|
{
|
|
AZStd::string textString;
|
|
EBUS_EVENT_ID_RESULT(textString, m_textEntity, UiTextBus, GetText);
|
|
|
|
// if a C++ callback is registered for OnEndEdit then call it
|
|
if (m_onEndEdit)
|
|
{
|
|
// pass the entered text string to the C++ callback
|
|
m_onEndEdit(GetEntityId(), textString);
|
|
}
|
|
|
|
// Tell any action listeners that the edit ended
|
|
if (!m_endEditAction.empty())
|
|
{
|
|
// canvas listeners will get the action name (e.g. something like "EmailEntered") plus
|
|
// the ID of this entity.
|
|
AZ::EntityId canvasEntityId;
|
|
EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId);
|
|
EBUS_EVENT_ID(canvasEntityId, UiCanvasNotificationBus, OnAction, GetEntityId(), m_endEditAction);
|
|
}
|
|
|
|
EBUS_EVENT_ID(GetEntityId(), UiTextInputNotificationBus, OnTextInputEndEdit, textString);
|
|
|
|
// clear the selection highlight
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, ClearSelectionRange);
|
|
|
|
m_textCursorPos = m_textSelectionStartPos = -1;
|
|
|
|
if (m_isTextInputStarted)
|
|
{
|
|
AzFramework::InputTextEntryRequestBus::Broadcast(&AzFramework::InputTextEntryRequests::TextEntryStop);
|
|
m_isTextInputStarted = false;
|
|
}
|
|
|
|
m_isEditing = false;
|
|
|
|
// force re-evaluation of whether text or placeholder text should be displayed
|
|
m_childTextStateDirtyFlag = true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// calculate how much we have dragged along the text
|
|
float UiTextInputComponent::GetValidDragDistanceInPixels(AZ::Vector2 startPoint, AZ::Vector2 endPoint)
|
|
{
|
|
const float validDragRatio = 0.5f;
|
|
|
|
// convert the drag vector to local space
|
|
AZ::Matrix4x4 transformFromViewport;
|
|
EBUS_EVENT_ID(GetEntityId(), UiTransformBus, GetTransformFromViewport, transformFromViewport);
|
|
AZ::Vector2 dragVec = endPoint - startPoint;
|
|
AZ::Vector3 dragVec3(dragVec.GetX(), dragVec.GetY(), 0.0f);
|
|
AZ::Vector3 localDragVec = transformFromViewport.Multiply3x3(dragVec3);
|
|
|
|
// the text input component only supports drag along the x axis so zero the y axis
|
|
localDragVec.SetY(0.0f);
|
|
|
|
// convert back to viewport space
|
|
AZ::Matrix4x4 transformToViewport;
|
|
EBUS_EVENT_ID(GetEntityId(), UiTransformBus, GetTransformToViewport, transformToViewport);
|
|
AZ::Vector3 validDragVec = transformToViewport.Multiply3x3(localDragVec);
|
|
|
|
float validDistance = validDragVec.GetLengthSq();
|
|
float totalDistance = dragVec.GetLengthSq();
|
|
|
|
// if they are not dragging mostly in a valid direction then ignore the drag
|
|
if (validDistance / totalDistance < validDragRatio)
|
|
{
|
|
validDistance = 0.0f;
|
|
}
|
|
|
|
// return the valid drag distance
|
|
return validDistance;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::CheckForDragOrHandOffToParent(AZ::Vector2 point)
|
|
{
|
|
AZ::EntityId parentDraggable;
|
|
EBUS_EVENT_ID_RESULT(parentDraggable, GetEntityId(), UiElementBus, FindParentInteractableSupportingDrag, point);
|
|
|
|
// if this interactable is inside another interactable that supports drag then we use
|
|
// a threshold value before starting a drag on this interactable
|
|
const float normalDragThreshold = 0.0f;
|
|
const float containedDragThreshold = 5.0f;
|
|
|
|
float dragThreshold = normalDragThreshold;
|
|
if (parentDraggable.IsValid())
|
|
{
|
|
dragThreshold = containedDragThreshold;
|
|
}
|
|
|
|
// calculate how much we have dragged along the axis of the slider
|
|
float validDragDistance = GetValidDragDistanceInPixels(m_pressedPoint, point);
|
|
|
|
// only enter drag mode if we dragged above the threshold AND there is something to select
|
|
AZStd::string textString;
|
|
EBUS_EVENT_ID_RESULT(textString, m_textEntity, UiTextBus, GetText);
|
|
|
|
if (validDragDistance > dragThreshold && !textString.empty())
|
|
{
|
|
// we dragged above the threshold value along axis of slider
|
|
m_isDragging = true;
|
|
|
|
// enter editing state if we are not already in it
|
|
if (!m_isEditing)
|
|
{
|
|
BeginEditState();
|
|
}
|
|
}
|
|
else if (parentDraggable.IsValid())
|
|
{
|
|
// offer the parent draggable the chance to become the active interactable
|
|
bool handOff = false;
|
|
EBUS_EVENT_ID_RESULT(handOff, parentDraggable, UiInteractableBus,
|
|
OfferDragHandOff, GetEntityId(), m_pressedPoint, point, containedDragThreshold);
|
|
|
|
if (handOff)
|
|
{
|
|
// interaction has been handed off to a container entity
|
|
m_isPressed = false;
|
|
EndEditState();
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::OnReplacementCharacterChange()
|
|
{
|
|
if (m_replacementCharacter == '\0')
|
|
{
|
|
m_replacementCharacter = defaultReplacementChar;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::UpdateDisplayedTextFunction()
|
|
{
|
|
// If we're a password input box then we need to set up a callback to allow us to change how
|
|
// the text stored in our child component is displayed before rendering.
|
|
if (m_isPasswordField)
|
|
{
|
|
// Use a lambda here so we can easily access our instance to retrieve the
|
|
// currently configured replacement character
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, SetDisplayedTextFunction,
|
|
[this](const AZStd::string& originalText)
|
|
{
|
|
// NOTE: this assumes the uint32_t can be interpreted as a wchar_t, it seems to
|
|
// work for cases tested but may not in general.
|
|
wchar_t wcharString[2] = { static_cast<wchar_t>(this->GetReplacementCharacter()), 0 };
|
|
AZStd::string replacementCharString;
|
|
AZStd::to_string(replacementCharString, { wcharString, 1 });
|
|
|
|
int numReplacementChars = LyShine::GetUtf8StringLength(originalText);
|
|
|
|
AZStd::string replacedString;
|
|
replacedString.reserve(numReplacementChars * replacementCharString.length());
|
|
for (int i = 0; i < numReplacementChars; i++)
|
|
{
|
|
replacedString += replacementCharString;
|
|
}
|
|
|
|
return replacedString;
|
|
});
|
|
}
|
|
else
|
|
{
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, SetDisplayedTextFunction, nullptr);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiTextInputComponent::EntityComboBoxVec UiTextInputComponent::PopulateTextEntityList()
|
|
{
|
|
EntityComboBoxVec result;
|
|
AZStd::vector<AZ::EntityId> entityIdList;
|
|
|
|
// add a first entry for "None"
|
|
result.push_back(AZStd::make_pair(AZ::EntityId(AZ::EntityId()), "<None>"));
|
|
|
|
// allow the destination to be the same entity as the source by
|
|
// adding this entity (if it has a text component)
|
|
if (UiTextBus::FindFirstHandler(GetEntityId()))
|
|
{
|
|
result.push_back(AZStd::make_pair(AZ::EntityId(GetEntityId()), GetEntity()->GetName()));
|
|
}
|
|
|
|
// Add all descendant elements that have Text components to the lists
|
|
AddDescendantTextElements(GetEntityId(), result);
|
|
|
|
return result;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiInteractableStatesInterface::State UiTextInputComponent::ComputeInteractableState()
|
|
{
|
|
// This currently happens every frame. Needs optimization to just happen on events
|
|
|
|
UiInteractableStatesInterface::State state = UiInteractableStatesInterface::StateNormal;
|
|
|
|
if (!m_isHandlingEvents)
|
|
{
|
|
// not handling events, use disabled state
|
|
state = UiInteractableStatesInterface::StateDisabled;
|
|
}
|
|
else if (m_isPressed && m_isHover)
|
|
{
|
|
// We only use the pressed state when the state is pressed AND the mouse is over the rect
|
|
state = UiInteractableStatesInterface::StatePressed;
|
|
}
|
|
else if (m_isHover || m_isPressed || m_isEditing)
|
|
{
|
|
// we use the hover state for normal hover but also if the state is pressed but
|
|
// the mouse is outside the rect, and also if the text is being edited
|
|
state = UiInteractableStatesInterface::StateHover;
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// PROTECTED STATIC MEMBER FUNCTIONS
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void UiTextInputComponent::Reflect(AZ::ReflectContext* context)
|
|
{
|
|
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
|
|
if (serializeContext)
|
|
{
|
|
serializeContext->Class<UiTextInputComponent, UiInteractableComponent>()
|
|
->Version(8, &VersionConverter)
|
|
// Elements group
|
|
->Field("Text", &UiTextInputComponent::m_textEntity)
|
|
->Field("PlaceHolderText", &UiTextInputComponent::m_placeHolderTextEntity)
|
|
// Text editing group
|
|
->Field("TextSelectionColor", &UiTextInputComponent::m_textSelectionColor)
|
|
->Field("TextCursorColor", &UiTextInputComponent::m_textCursorColor)
|
|
->Field("MaxStringLength", &UiTextInputComponent::m_maxStringLength)
|
|
->Field("CursorBlinkInterval", &UiTextInputComponent::m_cursorBlinkInterval)
|
|
->Field("IsPasswordField", &UiTextInputComponent::m_isPasswordField)
|
|
->Field("ReplacementCharacter", &UiTextInputComponent::m_replacementCharacter)
|
|
->Field("ClipInputText", &UiTextInputComponent::m_clipInputText)
|
|
->Field("EnableClipboard", &UiTextInputComponent::m_enableClipboard)
|
|
// Actions group
|
|
->Field("ChangeAction", &UiTextInputComponent::m_changeAction)
|
|
->Field("EndEditAction", &UiTextInputComponent::m_endEditAction)
|
|
->Field("EnterAction", &UiTextInputComponent::m_enterAction);
|
|
|
|
AZ::EditContext* ec = serializeContext->GetEditContext();
|
|
if (ec)
|
|
{
|
|
auto editInfo = ec->Class<UiTextInputComponent>("TextInput", "An interactable component for editing a text string.");
|
|
|
|
editInfo->ClassElement(AZ::Edit::ClassElements::EditorData, "")
|
|
->Attribute(AZ::Edit::Attributes::Category, "UI")
|
|
->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/UiTextInput.png")
|
|
->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/UiTextInput.png")
|
|
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("UI", 0x27ff46b0))
|
|
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
|
|
|
|
// Elements group
|
|
{
|
|
editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Elements")
|
|
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
|
|
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiTextInputComponent::m_textEntity, "Text", "The UI element to hold the entered text.")
|
|
->Attribute(AZ::Edit::Attributes::EnumValues, &UiTextInputComponent::PopulateTextEntityList);
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiTextInputComponent::m_placeHolderTextEntity, "Placeholder text", "The UI element to display the placeholder text.")
|
|
->Attribute(AZ::Edit::Attributes::EnumValues, &UiTextInputComponent::PopulateTextEntityList);
|
|
}
|
|
|
|
// Text Editing group
|
|
{
|
|
editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Text editing")
|
|
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
|
|
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::Color, &UiTextInputComponent::m_textSelectionColor,
|
|
"Selection color", "The text selection color.");
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::Color, &UiTextInputComponent::m_textCursorColor,
|
|
"Cursor color", "The text cursor color.");
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::SpinBox, &UiTextInputComponent::m_cursorBlinkInterval,
|
|
"Cursor blink time", "The cursor blink interval.")
|
|
->Attribute(AZ::Edit::Attributes::Min, 0.0f)
|
|
->Attribute(AZ::Edit::Attributes::Step, 0.1f);
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::SpinBox, &UiTextInputComponent::m_maxStringLength,
|
|
"Max char count", "The maximum string length that can be entered. For unlimited enter -1.")
|
|
->Attribute(AZ::Edit::Attributes::Min, -1)
|
|
->Attribute(AZ::Edit::Attributes::Step, 1);
|
|
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiTextInputComponent::m_isPasswordField,
|
|
"Is password field", "A password field hides the entered text.")
|
|
->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c));
|
|
editInfo->DataElement(AZ_CRC("Char", 0x8cfe579f), &UiTextInputComponent::m_replacementCharacter,
|
|
"Replacement character", "The replacement character used to hide password text.")
|
|
->Attribute(AZ::Edit::Attributes::ChangeNotify, &UiTextInputComponent::OnReplacementCharacterChange)
|
|
->Attribute(AZ::Edit::Attributes::Visibility, &UiTextInputComponent::GetIsPasswordField);
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiTextInputComponent::m_clipInputText,
|
|
"Clip input text", "When checked, the input text is clipped to this element's rect.");
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiTextInputComponent::m_enableClipboard,
|
|
"Enable clipboard", "When checked, Ctrl-C, Ctrl-X, and Ctrl-V events will be handled");
|
|
}
|
|
|
|
// Actions group
|
|
{
|
|
editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Actions")
|
|
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
|
|
|
|
editInfo->DataElement(0, &UiTextInputComponent::m_changeAction, "Change", "The action name triggered on each character typed.");
|
|
editInfo->DataElement(0, &UiTextInputComponent::m_endEditAction, "End edit", "The action name triggered on either focus change or enter.");
|
|
editInfo->DataElement(0, &UiTextInputComponent::m_enterAction, "Enter", "The action name triggered when enter is pressed.");
|
|
}
|
|
}
|
|
}
|
|
|
|
AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context);
|
|
if (behaviorContext)
|
|
{
|
|
behaviorContext->EBus<UiTextInputBus>("UiTextInputBus")
|
|
->Event("GetTextSelectionColor", &UiTextInputBus::Events::GetTextSelectionColor)
|
|
->Event("SetTextSelectionColor", &UiTextInputBus::Events::SetTextSelectionColor)
|
|
->Event("GetTextCursorColor", &UiTextInputBus::Events::GetTextCursorColor)
|
|
->Event("SetTextCursorColor", &UiTextInputBus::Events::SetTextCursorColor)
|
|
->Event("GetCursorBlinkInterval", &UiTextInputBus::Events::GetCursorBlinkInterval)
|
|
->Event("SetCursorBlinkInterval", &UiTextInputBus::Events::SetCursorBlinkInterval)
|
|
->Event("GetMaxStringLength", &UiTextInputBus::Events::GetMaxStringLength)
|
|
->Event("SetMaxStringLength", &UiTextInputBus::Events::SetMaxStringLength)
|
|
->Event("GetChangeAction", &UiTextInputBus::Events::GetChangeAction)
|
|
->Event("SetChangeAction", &UiTextInputBus::Events::SetChangeAction)
|
|
->Event("GetEndEditAction", &UiTextInputBus::Events::GetEndEditAction)
|
|
->Event("SetEndEditAction", &UiTextInputBus::Events::SetEndEditAction)
|
|
->Event("GetEnterAction", &UiTextInputBus::Events::GetEnterAction)
|
|
->Event("SetEnterAction", &UiTextInputBus::Events::SetEnterAction)
|
|
->Event("GetTextEntity", &UiTextInputBus::Events::GetTextEntity)
|
|
->Event("SetTextEntity", &UiTextInputBus::Events::SetTextEntity)
|
|
->Event("GetText", &UiTextInputBus::Events::GetText)
|
|
->Event("SetText", &UiTextInputBus::Events::SetText)
|
|
->Event("GetPlaceHolderTextEntity", &UiTextInputBus::Events::GetPlaceHolderTextEntity)
|
|
->Event("SetPlaceHolderTextEntity", &UiTextInputBus::Events::SetPlaceHolderTextEntity)
|
|
->Event("GetIsPasswordField", &UiTextInputBus::Events::GetIsPasswordField)
|
|
->Event("SetIsPasswordField", &UiTextInputBus::Events::SetIsPasswordField)
|
|
->Event("GetReplacementCharacter", &UiTextInputBus::Events::GetReplacementCharacter)
|
|
->Event("SetReplacementCharacter", &UiTextInputBus::Events::SetReplacementCharacter)
|
|
->Event("GetIsClipboardEnabled", &UiTextInputBus::Events::GetIsClipboardEnabled)
|
|
->Event("SetIsClipboardEnabled", &UiTextInputBus::Events::SetIsClipboardEnabled)
|
|
->VirtualProperty("TextSelectionColor", "GetTextSelectionColor", "SetTextSelectionColor")
|
|
->VirtualProperty("TextCursorColor", "GetTextCursorColor", "SetTextCursorColor")
|
|
->VirtualProperty("CursorBlinkInterval", "GetCursorBlinkInterval", "SetCursorBlinkInterval")
|
|
->VirtualProperty("MaxStringLength", "GetMaxStringLength", "SetMaxStringLength");
|
|
|
|
behaviorContext->Class<UiTextInputComponent>()->RequestBus("UiTextInputBus");
|
|
|
|
behaviorContext->EBus<UiTextInputNotificationBus>("UiTextInputNotificationBus")
|
|
->Handler<BehaviorUiTextInputNotificationBusHandler>();
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// PRIVATE MEMBER FUNCTIONS
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::ChangeText(const AZStd::string& textString)
|
|
{
|
|
// For user-inputted text, we assume that users don't want to input
|
|
// text as styling markup (but rather plain-text).
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, SetTextWithFlags, textString, UiTextInterface::SetEscapeMarkup);
|
|
|
|
// if a C++ callback is registered for OnChange then call it
|
|
if (m_onChange)
|
|
{
|
|
// pass the entered text string to the C++ callback
|
|
m_onChange(GetEntityId(), textString);
|
|
}
|
|
|
|
// Tell any action listeners about the event
|
|
if (!m_changeAction.empty())
|
|
{
|
|
// canvas listeners will get the action name (e.g. something like "EmailEdited") plus
|
|
// the ID of this entity.
|
|
AZ::EntityId canvasEntityId;
|
|
EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId);
|
|
EBUS_EVENT_ID(canvasEntityId, UiCanvasNotificationBus, OnAction, GetEntityId(), m_changeAction);
|
|
}
|
|
|
|
EBUS_EVENT_ID(GetEntityId(), UiTextInputNotificationBus, OnTextInputChange, textString);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::ResetCursorBlink()
|
|
{
|
|
m_textCursorColor.SetA(1.0f);
|
|
m_cursorBlinkStartTime = 0.0f;
|
|
EBUS_EVENT_ID(m_textEntity, UiTextBus, SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, m_textCursorColor);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiTextInputComponent::CheckStartTextInput()
|
|
{
|
|
// We do not bring up the on-screen keyboard when a drag is started, only on a "click" or at
|
|
// the end of a drag. But a drag begin can cause BeginEditState to be called. So we can begin
|
|
// edit state before we bring up the on-screen keyboard. So here we test if it is time to bring
|
|
// up the keyboard.
|
|
if (m_isEditing && !m_isTextInputStarted)
|
|
{
|
|
// ensure the on-screen keyboard is shown on mobile and console platforms
|
|
AzFramework::InputTextEntryRequests::VirtualKeyboardOptions options;
|
|
|
|
AZStd::string textString;
|
|
EBUS_EVENT_ID_RESULT(textString, m_textEntity, UiTextBus, GetText);
|
|
options.m_initialText = Utf8SubString(textString, m_textCursorPos, m_textSelectionStartPos);
|
|
|
|
AZ::EntityId canvasEntityId;
|
|
EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId);
|
|
|
|
// Calculate height available for virtual keyboard. In game mode, canvas size is the same as viewport size
|
|
AZ::Vector2 canvasSize;
|
|
EBUS_EVENT_ID_RESULT(canvasSize, canvasEntityId, UiCanvasBus, GetCanvasSize);
|
|
UiTransformInterface::RectPoints rectPoints;
|
|
EBUS_EVENT_ID(GetEntityId(), UiTransformBus, GetViewportSpacePoints, rectPoints);
|
|
const AZ::Vector2 bottomRight = rectPoints.GetAxisAlignedBottomRight();
|
|
options.m_normalizedMinY = (canvasSize.GetY() > 0.0f) ? bottomRight.GetY() / canvasSize.GetY() : 0.0f;
|
|
|
|
EBUS_EVENT_ID_RESULT(options.m_localUserId, canvasEntityId, UiCanvasBus, GetLocalUserIdInputFilter);
|
|
|
|
AzFramework::InputTextEntryRequestBus::Broadcast(&AzFramework::InputTextEntryRequests::TextEntryStart, options);
|
|
|
|
m_isTextInputStarted = true;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// PRIVATE STATIC MEMBER FUNCTIONS
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiTextInputComponent::VersionConverter(AZ::SerializeContext& context,
|
|
AZ::SerializeContext::DataElementNode& classElement)
|
|
{
|
|
// conversion from version 1:
|
|
// - Need to convert CryString elements to AZStd::string
|
|
// - Need to convert Color to Color and Alpha
|
|
// conversion from version 1 or 2 to current:
|
|
// - Need to convert CryString ActionName elements to AZStd::string
|
|
AZ_Assert(classElement.GetVersion() > 2, "Unsupported UiTextInputComponent version: %d", classElement.GetVersion());
|
|
|
|
// conversion from version 1, 2 or 3 to current:
|
|
// - Need to convert AZStd::string sprites to AzFramework::SimpleAssetReference<LmbrCentral::TextureAsset>
|
|
if (classElement.GetVersion() <= 3)
|
|
{
|
|
if (!LyShine::ConvertSubElementFromAzStringToAssetRef<LmbrCentral::TextureAsset>(context, classElement, "SelectedSprite"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!LyShine::ConvertSubElementFromAzStringToAssetRef<LmbrCentral::TextureAsset>(context, classElement, "PressedSprite"))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Conversion from version 4 to 5:
|
|
if (classElement.GetVersion() < 5)
|
|
{
|
|
// find the base class (AZ::Component)
|
|
// NOTE: in very old versions there may not be a base class because the base class was not serialized
|
|
int componentBaseClassIndex = classElement.FindElement(AZ_CRC("BaseClass1", 0xd4925735));
|
|
|
|
// If there was a base class, make a copy and remove it
|
|
AZ::SerializeContext::DataElementNode componentBaseClassNode;
|
|
if (componentBaseClassIndex != -1)
|
|
{
|
|
// make a local copy of the component base class node
|
|
componentBaseClassNode = classElement.GetSubElement(componentBaseClassIndex);
|
|
|
|
// remove the component base class from the button
|
|
classElement.RemoveElement(componentBaseClassIndex);
|
|
}
|
|
|
|
// Add a new base class (UiInteractableComponent)
|
|
int interactableBaseClassIndex = classElement.AddElement<UiInteractableComponent>(context, "BaseClass1");
|
|
AZ::SerializeContext::DataElementNode& interactableBaseClassNode = classElement.GetSubElement(interactableBaseClassIndex);
|
|
|
|
// if there was previously a base class...
|
|
if (componentBaseClassIndex != -1)
|
|
{
|
|
// copy the component base class into the new interactable base class
|
|
// Since AZ::Component is now the base class of UiInteractableComponent
|
|
interactableBaseClassNode.AddElement(componentBaseClassNode);
|
|
}
|
|
|
|
// Move the selected/hover state to the base class
|
|
if (!UiSerialize::MoveToInteractableStateActions(context, classElement, "HoverStateActions",
|
|
"SelectedColor", "SelectedAlpha", "SelectedSprite"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Move the pressed state to the base class
|
|
if (!UiSerialize::MoveToInteractableStateActions(context, classElement, "PressedStateActions",
|
|
"PressedColor", "PressedAlpha", "PressedSprite"))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Conversion from version 5 to 6:
|
|
if (classElement.GetVersion() < 6)
|
|
{
|
|
int clipTextIndex = classElement.AddElement<bool>(context, "ClipInputText");
|
|
|
|
if (clipTextIndex == -1)
|
|
{
|
|
// Error adding the new sub element
|
|
AZ_Error("Serialization", false, "Failed to create ClipInputText node");
|
|
return false;
|
|
}
|
|
|
|
AZ::SerializeContext::DataElementNode& clipTextNode = classElement.GetSubElement(clipTextIndex);
|
|
clipTextNode.SetData(context, false);
|
|
}
|
|
|
|
// conversion from version 6 to 7: Need to convert ColorF to AZ::Color
|
|
if (classElement.GetVersion() < 7)
|
|
{
|
|
if (!LyShine::ConvertSubElementFromColorFToAzColor(context, classElement, "TextSelectionColor"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!LyShine::ConvertSubElementFromColorFToAzColor(context, classElement, "TextCursorColor"))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Conversion from 7 to 8: Need to convert char to uint32_t
|
|
if (classElement.GetVersion() < 8)
|
|
{
|
|
if (!LyShine::ConvertSubElementFromCharToUInt32(context, classElement, "ReplacementCharacter"))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|