/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include "LyShine_precompiled.h" #include "UiTextInputComponent.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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) { int 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. Unicode::CIterator pChar(utf8String.c_str()); 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(activeModifierKeys) & static_cast(AzFramework::ModifierKeyMask::ShiftAny)) != 0; const bool isLCTRLModifierActive = (static_cast(activeModifierKeys) & static_cast(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(activeModifierKeys) & static_cast(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 += 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(this->GetReplacementCharacter()), 0 }; AZStd::string replacementCharString(CryStringUtils::WStrToUTF8(wcharString)); 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 entityIdList; // add a first entry for "None" result.push_back(AZStd::make_pair(AZ::EntityId(AZ::EntityId()), "")); // 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(context); if (serializeContext) { serializeContext->Class() ->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("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(context); if (behaviorContext) { behaviorContext->EBus("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()->RequestBus("UiTextInputBus"); behaviorContext->EBus("UiTextInputNotificationBus") ->Handler(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // 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); UiTransformInterface::RectPoints rectPoints; EBUS_EVENT_ID(GetEntityId(), UiTransformBus, GetViewportSpacePoints, rectPoints); const AZ::Vector2 bottomRight = rectPoints.GetAxisAlignedBottomRight(); options.m_normalizedMinY = bottomRight.GetY() / static_cast(gEnv->pRenderer->GetHeight()); AZ::EntityId canvasEntityId; EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId); 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 if (classElement.GetVersion() <= 1) { if (!LyShine::ConvertSubElementFromCryStringToAzString(context, classElement, "SelectedSprite")) { return false; } if (!LyShine::ConvertSubElementFromCryStringToAzString(context, classElement, "PressedSprite")) { return false; } if (!LyShine::ConvertSubElementFromColorToColorPlusAlpha(context, classElement, "SelectedColor", "SelectedAlpha")) { return false; } if (!LyShine::ConvertSubElementFromColorToColorPlusAlpha(context, classElement, "PressedColor", "PressedAlpha")) { return false; } if (!LyShine::ConvertSubElementFromCryStringToChar(context, classElement, "ReplacementCharacter", defaultReplacementChar)) { return false; } } // conversion from version 1 or 2 to current: // - Need to convert CryString ActionName elements to AZStd::string if (classElement.GetVersion() <= 2) { if (!LyShine::ConvertSubElementFromCryStringToAzString(context, classElement, "ChangeAction")) { return false; } if (!LyShine::ConvertSubElementFromCryStringToAzString(context, classElement, "EndEditAction")) { return false; } if (!LyShine::ConvertSubElementFromCryStringToAzString(context, classElement, "EnterAction")) { return false; } } // conversion from version 1, 2 or 3 to current: // - Need to convert AZStd::string sprites to AzFramework::SimpleAssetReference if (classElement.GetVersion() <= 3) { if (!LyShine::ConvertSubElementFromAzStringToAssetRef(context, classElement, "SelectedSprite")) { return false; } if (!LyShine::ConvertSubElementFromAzStringToAssetRef(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(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(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; }