UiTextInputComponent.cpp 63 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include "UiTextInputComponent.h"
  9. #include <AzCore/Math/Crc.h>
  10. #include <AzCore/Serialization/SerializeContext.h>
  11. #include <AzCore/Serialization/EditContext.h>
  12. #include <AzCore/std/string/conversions.h>
  13. #include <AzCore/RTTI/BehaviorContext.h>
  14. #include <AzCore/Component/ComponentApplicationBus.h>
  15. #include <AzCore/Time/ITime.h>
  16. #include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
  17. #include <LyShine/Bus/UiElementBus.h>
  18. #include <LyShine/Bus/UiTransformBus.h>
  19. #include <LyShine/Bus/UiVisualBus.h>
  20. #include <LyShine/Bus/UiCanvasBus.h>
  21. #include <LyShine/Bus/UiTextBus.h>
  22. #include <LyShine/ISprite.h>
  23. #include <LyShine/IDraw2d.h>
  24. #include <LyShine/UiSerializeHelpers.h>
  25. #include "UiNavigationHelpers.h"
  26. #include "UiSerialize.h"
  27. #include "Sprite.h"
  28. #include "StringUtfUtils.h"
  29. #include "UiClipboard.h"
  30. namespace
  31. {
  32. // Orange color from the canvas editor style guide
  33. const AZ::Color defaultSelectionColor(255.0f / 255.0f, 153.0f / 255.0f, 0.0f / 255.0f, 1.0f);
  34. // White color from the canvas editor style guide
  35. const AZ::Color defaultCursorColor(238.0f / 255.0f, 238.0f / 255.0f, 238.0f / 255.0f, 1.0f);
  36. const uint32_t defaultReplacementChar('*');
  37. // Add all descendant elements that support the UiTextBus to a list of pairs of
  38. // entity ID and string.
  39. void AddDescendantTextElements(AZ::EntityId entity,
  40. UiTextInputComponent::EntityComboBoxVec& result)
  41. {
  42. // Get a list of all descendant elements that support the UiTextBus
  43. LyShine::EntityArray matchingElements;
  44. UiElementBus::Event(
  45. entity,
  46. &UiElementBus::Events::FindDescendantElements,
  47. [](const AZ::Entity* descendant)
  48. {
  49. return UiTextBus::FindFirstHandler(descendant->GetId()) != nullptr;
  50. },
  51. matchingElements);
  52. // add their names to the StringList and their IDs to the id list
  53. for (auto childEntity : matchingElements)
  54. {
  55. result.push_back(AZStd::make_pair(AZ::EntityId(childEntity->GetId()), childEntity->GetName()));
  56. }
  57. }
  58. //! \brief Given a UTF8 string and index, return the raw string buffer index that maps to the UTF8 index.
  59. int GetCharArrayIndexFromUtf8CharIndex(const AZStd::string& utf8String, const uint utf8Index)
  60. {
  61. uint utfIndexIter = 0;
  62. int rawIndex = 0;
  63. const AZStd::string::size_type stringLength = utf8String.length();
  64. if (stringLength > 0 && stringLength >= utf8Index)
  65. {
  66. // Iterate over the string until the given index is found.
  67. Utf8::Unchecked::octet_iterator pChar(utf8String.data());
  68. while (uint32_t ch = *pChar)
  69. {
  70. if (utf8Index == utfIndexIter)
  71. {
  72. break;
  73. }
  74. ++utfIndexIter;
  75. // Add up the size of the multibyte chars along the way,
  76. // which will give us the "raw" string buffer index of where
  77. // the given index maps to.
  78. rawIndex += LyShine::GetMultiByteCharSize(ch);
  79. ++pChar;
  80. }
  81. }
  82. return rawIndex;
  83. }
  84. //! \brief Removes a range of UTF8 code points using the given indices.
  85. //! The given indices are code-point indices and not raw (byte) indices.
  86. void RemoveUtf8CodePointsByIndex(AZStd::string& utf8String, int index1, int index2)
  87. {
  88. const int minSelectIndex = min(index1, index2);
  89. const int maxSelectIndex = max(index1, index2);
  90. const int left = GetCharArrayIndexFromUtf8CharIndex(utf8String, minSelectIndex);
  91. const int right = GetCharArrayIndexFromUtf8CharIndex(utf8String, maxSelectIndex);
  92. utf8String.erase(left, right - left);
  93. }
  94. //! \brief Returns a UTF8 sub-string using the given indices.
  95. //! The given indices are code-point indices and not raw (byte) indices.
  96. AZStd::string Utf8SubString(const AZStd::string& utf8String, int utf8CharIndexStart, int utf8CharIndexEnd)
  97. {
  98. const int minCharIndex = min(utf8CharIndexStart, utf8CharIndexEnd);
  99. const int maxCharIndex = max(utf8CharIndexStart, utf8CharIndexEnd);
  100. const int left = GetCharArrayIndexFromUtf8CharIndex(utf8String, minCharIndex);
  101. const int right = GetCharArrayIndexFromUtf8CharIndex(utf8String, maxCharIndex);
  102. return utf8String.substr(left, right - left);
  103. }
  104. //! \brief Convenience method for erasing a range of text and updating the given selection indices accordingly.
  105. void EraseAndUpdateSelectionRange(AZStd::string& utf8String, int& endSelectIndex, int& startSelectIndex)
  106. {
  107. RemoveUtf8CodePointsByIndex(utf8String, endSelectIndex, startSelectIndex);
  108. endSelectIndex = startSelectIndex = min(endSelectIndex, startSelectIndex);
  109. }
  110. } // anonymous namespace
  111. ////////////////////////////////////////////////////////////////////////////////////////////////////
  112. //! UiTextInputNotificationBus Behavior context handler class
  113. class BehaviorUiTextInputNotificationBusHandler
  114. : public UiTextInputNotificationBus::Handler
  115. , public AZ::BehaviorEBusHandler
  116. {
  117. public:
  118. AZ_EBUS_BEHAVIOR_BINDER(BehaviorUiTextInputNotificationBusHandler, "{5ED20B32-95E2-4EBB-8874-7E780306F7F0}", AZ::SystemAllocator,
  119. OnTextInputChange, OnTextInputEndEdit, OnTextInputEnter);
  120. void OnTextInputChange(const AZStd::string& textString) override
  121. {
  122. Call(FN_OnTextInputChange, textString);
  123. }
  124. void OnTextInputEndEdit(const AZStd::string& textString) override
  125. {
  126. Call(FN_OnTextInputEndEdit, textString);
  127. }
  128. void OnTextInputEnter(const AZStd::string& textString) override
  129. {
  130. Call(FN_OnTextInputEnter, textString);
  131. }
  132. };
  133. ////////////////////////////////////////////////////////////////////////////////////////////////////
  134. // PUBLIC MEMBER FUNCTIONS
  135. ////////////////////////////////////////////////////////////////////////////////////////////////////
  136. ////////////////////////////////////////////////////////////////////////////////////////////////////
  137. UiTextInputComponent::UiTextInputComponent()
  138. : m_isDragging(false)
  139. , m_isEditing(false)
  140. , m_isTextInputStarted(false)
  141. , m_textCursorPos(-1)
  142. , m_textSelectionStartPos(-1)
  143. , m_cursorBlinkStartTime(0.0f)
  144. , m_textEntity()
  145. , m_placeHolderTextEntity()
  146. , m_textSelectionColor(defaultSelectionColor)
  147. , m_textCursorColor(defaultCursorColor)
  148. , m_maxStringLength(-1)
  149. , m_cursorBlinkInterval(1.0f)
  150. , m_childTextStateDirtyFlag(true)
  151. , m_onChange(nullptr)
  152. , m_onEndEdit(nullptr)
  153. , m_onEnter(nullptr)
  154. , m_replacementCharacter(defaultReplacementChar)
  155. , m_isPasswordField(false)
  156. , m_clipInputText(true)
  157. , m_enableClipboard(true)
  158. {
  159. }
  160. ////////////////////////////////////////////////////////////////////////////////////////////////////
  161. UiTextInputComponent::~UiTextInputComponent()
  162. {
  163. if (m_isEditing)
  164. {
  165. AzFramework::InputTextEntryRequestBus::Broadcast(&AzFramework::InputTextEntryRequests::TextEntryStop);
  166. }
  167. }
  168. ////////////////////////////////////////////////////////////////////////////////////////////////////
  169. bool UiTextInputComponent::HandlePressed(AZ::Vector2 point, bool& shouldStayActive)
  170. {
  171. bool handled = UiInteractableComponent::HandlePressed(point, shouldStayActive);
  172. if (handled)
  173. {
  174. // clear the dragging flag, we are not dragging until we detect a drag
  175. m_isDragging = false;
  176. // the text input field will stay active after released
  177. shouldStayActive = true;
  178. // store the character position where the press corresponds to in the text string
  179. UiTextBus::EventResult(m_textCursorPos, m_textEntity, &UiTextBus::Events::GetCharIndexFromPoint, point, false);
  180. m_textSelectionStartPos = m_textCursorPos;
  181. }
  182. ResetCursorBlink();
  183. return handled;
  184. }
  185. ////////////////////////////////////////////////////////////////////////////////////////////////////
  186. bool UiTextInputComponent::HandleReleased(AZ::Vector2 point)
  187. {
  188. m_isPressed = false;
  189. m_isDragging = false;
  190. if (!m_isHandlingEvents)
  191. {
  192. return false;
  193. }
  194. if (!m_isEditing)
  195. {
  196. bool isInRect = false;
  197. UiTransformBus::EventResult(isInRect, GetEntityId(), &UiTransformBus::Events::IsPointInRect, point);
  198. if (isInRect)
  199. {
  200. BeginEditState();
  201. }
  202. else
  203. {
  204. // cancel the active status
  205. UiInteractableActiveNotificationBus::Event(GetEntityId(), &UiInteractableActiveNotificationBus::Events::ActiveCancelled);
  206. }
  207. }
  208. CheckStartTextInput();
  209. UiInteractableComponent::TriggerReleasedAction();
  210. return true;
  211. }
  212. ////////////////////////////////////////////////////////////////////////////////////////////////////
  213. bool UiTextInputComponent::HandleEnterPressed(bool& shouldStayActive)
  214. {
  215. bool handled = UiInteractableComponent::HandleEnterPressed(shouldStayActive);
  216. if (handled)
  217. {
  218. // the text input field will stay active after released
  219. shouldStayActive = true;
  220. AZStd::string textString;
  221. UiTextBus::EventResult(textString, m_textEntity, &UiTextBus::Events::GetText);
  222. // select all the text
  223. m_textCursorPos = 0;
  224. m_textSelectionStartPos = LyShine::GetUtf8StringLength(textString);
  225. }
  226. return handled;
  227. }
  228. ////////////////////////////////////////////////////////////////////////////////////////////////////
  229. bool UiTextInputComponent::HandleEnterReleased()
  230. {
  231. m_isPressed = false;
  232. if (!m_isHandlingEvents)
  233. {
  234. return false;
  235. }
  236. if (!m_isEditing)
  237. {
  238. BeginEditState();
  239. }
  240. CheckStartTextInput();
  241. UiInteractableComponent::TriggerReleasedAction();
  242. return true;
  243. }
  244. ////////////////////////////////////////////////////////////////////////////////////////////////////
  245. bool UiTextInputComponent::HandleAutoActivation()
  246. {
  247. if (!m_isHandlingEvents)
  248. {
  249. return false;
  250. }
  251. AZStd::string textString;
  252. UiTextBus::EventResult(textString, m_textEntity, &UiTextBus::Events::GetText);
  253. m_textCursorPos = LyShine::GetUtf8StringLength(textString);
  254. m_textSelectionStartPos = m_textCursorPos;
  255. if (!m_isEditing)
  256. {
  257. BeginEditState();
  258. }
  259. CheckStartTextInput();
  260. return true;
  261. }
  262. ////////////////////////////////////////////////////////////////////////////////////////////////////
  263. bool UiTextInputComponent::HandleTextInput(const AZStd::string& inputTextUTF8)
  264. {
  265. if (!m_isHandlingEvents)
  266. {
  267. return false;
  268. }
  269. // don't accept text input while in pressed state
  270. if (m_isPressed)
  271. {
  272. return false;
  273. }
  274. AZStd::string currentText;
  275. UiTextBus::EventResult(currentText, m_textEntity, &UiTextBus::Events::GetText);
  276. bool changedText = false;
  277. if (inputTextUTF8 == "\b" || inputTextUTF8 == "\x7f")
  278. {
  279. // backspace pressed, delete character before cursor or the selected range
  280. if (m_textCursorPos > 0 || m_textCursorPos != m_textSelectionStartPos)
  281. {
  282. if (m_textCursorPos != m_textSelectionStartPos)
  283. {
  284. // range is selected
  285. EraseAndUpdateSelectionRange(currentText, m_textCursorPos, m_textSelectionStartPos);
  286. }
  287. else
  288. {
  289. // "Select" one codepoint to erase (via backspace)
  290. m_textSelectionStartPos = m_textCursorPos - 1;
  291. EraseAndUpdateSelectionRange(currentText, m_textCursorPos, m_textSelectionStartPos);
  292. }
  293. UiTextBus::Event(
  294. m_textEntity, &UiTextBus::Events::SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, m_textCursorColor);
  295. changedText = true;
  296. }
  297. }
  298. // if inputTextUTF8 is a control character (a non printing character such as esc or tab) ignore it
  299. else if (inputTextUTF8.size() != 1 || !AZStd::is_cntrl(inputTextUTF8.at(0)))
  300. {
  301. // note currently we are treating the wchar passed in as a char, for localization
  302. // we need to use a wide string or utf8 string
  303. if (m_textCursorPos >= 0)
  304. {
  305. // if a range is selected then erase that first
  306. if (m_textCursorPos != m_textSelectionStartPos)
  307. {
  308. EraseAndUpdateSelectionRange(currentText, m_textCursorPos, m_textSelectionStartPos);
  309. changedText = true;
  310. }
  311. // only allow text to be added if there is no length limit or the length is under the limit
  312. if (m_maxStringLength < 0 || currentText.length() < m_maxStringLength)
  313. {
  314. int rawIndexPos = GetCharArrayIndexFromUtf8CharIndex(currentText, m_textCursorPos);
  315. if (rawIndexPos >= 0)
  316. {
  317. currentText.insert(rawIndexPos, inputTextUTF8);
  318. m_textCursorPos++;
  319. m_textSelectionStartPos = m_textCursorPos;
  320. UiTextBus::Event(
  321. m_textEntity, &UiTextBus::Events::SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, m_textCursorColor);
  322. changedText = true;
  323. }
  324. }
  325. }
  326. }
  327. if (changedText)
  328. {
  329. ChangeText(currentText);
  330. ResetCursorBlink();
  331. }
  332. return true;
  333. }
  334. ////////////////////////////////////////////////////////////////////////////////////////////////////
  335. bool UiTextInputComponent::HandleKeyInputBegan(const AzFramework::InputChannel::Snapshot& inputSnapshot, AzFramework::ModifierKeyMask activeModifierKeys)
  336. {
  337. if (!m_isHandlingEvents)
  338. {
  339. return false;
  340. }
  341. // don't accept character input while in pressed state
  342. if (m_isPressed)
  343. {
  344. return false;
  345. }
  346. bool result = true;
  347. int oldTextCursorPos = m_textCursorPos;
  348. int oldTextSelectionStartPos = m_textSelectionStartPos;
  349. const bool isShiftModifierActive = (static_cast<int>(activeModifierKeys) & static_cast<int>(AzFramework::ModifierKeyMask::ShiftAny)) != 0;
  350. const bool isLCTRLModifierActive = (static_cast<int>(activeModifierKeys) & static_cast<int>(AzFramework::ModifierKeyMask::CtrlAny)) != 0;
  351. const UiNavigationHelpers::Command command = UiNavigationHelpers::MapInputChannelIdToUiNavigationCommand(inputSnapshot.m_channelId, activeModifierKeys);
  352. if (command == UiNavigationHelpers::Command::Enter)
  353. {
  354. // enter was pressed
  355. AZStd::string textString;
  356. UiTextBus::EventResult(textString, m_textEntity, &UiTextBus::Events::GetText);
  357. // if a C++ callback is registered for OnEnter then call it
  358. if (m_onEnter)
  359. {
  360. // pass the entered text string to the C++ callback
  361. m_onEnter(GetEntityId(), textString);
  362. }
  363. // Tell any action listeners about the event
  364. if (!m_enterAction.empty())
  365. {
  366. // canvas listeners will get the action name (e.g. something like "EmailEntered") plus
  367. // the ID of this entity.
  368. AZ::EntityId canvasEntityId;
  369. UiElementBus::EventResult(canvasEntityId, GetEntityId(), &UiElementBus::Events::GetCanvasEntityId);
  370. UiCanvasNotificationBus::Event(canvasEntityId, &UiCanvasNotificationBus::Events::OnAction, GetEntityId(), m_enterAction);
  371. }
  372. UiTextInputNotificationBus::Event(GetEntityId(), &UiTextInputNotificationBus::Events::OnTextInputEnter, textString);
  373. // cancel the active status
  374. UiInteractableActiveNotificationBus::Event(GetEntityId(), &UiInteractableActiveNotificationBus::Events::ActiveCancelled);
  375. EndEditState();
  376. }
  377. else if (inputSnapshot.m_channelId == AzFramework::InputDeviceKeyboard::Key::NavigationDelete)
  378. {
  379. AZStd::string textString;
  380. UiTextBus::EventResult(textString, m_textEntity, &UiTextBus::Events::GetText);
  381. // Delete pressed, delete character after cursor or the selected range
  382. if (m_textCursorPos < LyShine::GetUtf8StringLength(textString) || m_textCursorPos != m_textSelectionStartPos)
  383. {
  384. if (m_textCursorPos != m_textSelectionStartPos)
  385. {
  386. // range is selected
  387. EraseAndUpdateSelectionRange(textString, m_textCursorPos, m_textSelectionStartPos);
  388. }
  389. else
  390. {
  391. // no range selected - delete character after cursor
  392. RemoveUtf8CodePointsByIndex(textString, m_textCursorPos, m_textCursorPos + 1);
  393. }
  394. ChangeText(textString);
  395. }
  396. }
  397. else if (command == UiNavigationHelpers::Command::Left || command == UiNavigationHelpers::Command::Right)
  398. {
  399. if (m_textCursorPos != m_textSelectionStartPos)
  400. {
  401. // Range is selected
  402. if (isShiftModifierActive)
  403. {
  404. // Move cursor to change selected range
  405. AZStd::string textString;
  406. UiTextBus::EventResult(textString, m_textEntity, &UiTextBus::Events::GetText);
  407. if (command == UiNavigationHelpers::Command::Left)
  408. {
  409. if (m_textCursorPos > 0)
  410. {
  411. --m_textCursorPos;
  412. }
  413. }
  414. else // UiNavigationHelpers::Command::Right
  415. {
  416. if (m_textCursorPos < LyShine::GetUtf8StringLength(textString))
  417. {
  418. ++m_textCursorPos;
  419. }
  420. }
  421. }
  422. else
  423. {
  424. // Place cursor at start or end of selection
  425. if (command == UiNavigationHelpers::Command::Left)
  426. {
  427. m_textCursorPos = min(m_textCursorPos, m_textSelectionStartPos);
  428. }
  429. else // eKI_Right
  430. {
  431. m_textCursorPos = max(m_textCursorPos, m_textSelectionStartPos);
  432. }
  433. m_textSelectionStartPos = m_textCursorPos;
  434. }
  435. }
  436. else
  437. {
  438. // No range selected, move cursor one character
  439. AZStd::string textString;
  440. UiTextBus::EventResult(textString, m_textEntity, &UiTextBus::Events::GetText);
  441. if (command == UiNavigationHelpers::Command::Left)
  442. {
  443. if (m_textCursorPos > 0)
  444. {
  445. --m_textCursorPos;
  446. }
  447. }
  448. else // eKI_Right
  449. {
  450. if (m_textCursorPos < LyShine::GetUtf8StringLength(textString))
  451. {
  452. ++m_textCursorPos;
  453. }
  454. }
  455. if (!isShiftModifierActive)
  456. {
  457. m_textSelectionStartPos = m_textCursorPos;
  458. }
  459. }
  460. }
  461. else if (command == UiNavigationHelpers::Command::Up || command == UiNavigationHelpers::Command::Down)
  462. {
  463. AZ::Vector2 currentPosition;
  464. UiTextBus::EventResult(currentPosition, m_textEntity, &UiTextBus::Events::GetPointFromCharIndex, m_textCursorPos);
  465. float fontSize;
  466. UiTextBus::EventResult(fontSize, m_textEntity, &UiTextBus::Events::GetFontSize);
  467. // To get the position of the cursor on the line above or below the
  468. // current cursor position, we add or subtract the font size,
  469. // depending on whether arrow key up or down is provided.
  470. if (command == UiNavigationHelpers::Command::Up)
  471. {
  472. fontSize *= -1.0f;
  473. }
  474. // Get the index that matches closest to the position directly above
  475. // or below the current cursor position.
  476. currentPosition.SetY(currentPosition.GetY() + fontSize);
  477. int adjustedIndex = 0;
  478. UiTextBus::EventResult(adjustedIndex, m_textEntity, &UiTextBus::Events::GetCharIndexFromCanvasSpacePoint, currentPosition, true);
  479. if (adjustedIndex != -1)
  480. {
  481. if (isShiftModifierActive)
  482. {
  483. m_textCursorPos = adjustedIndex;
  484. }
  485. else
  486. {
  487. result = m_textCursorPos != adjustedIndex;
  488. m_textCursorPos = m_textSelectionStartPos = adjustedIndex;
  489. }
  490. UiTextBus::Event(
  491. m_textEntity, &UiTextBus::Events::SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, m_textCursorColor);
  492. }
  493. else
  494. {
  495. result = isShiftModifierActive;
  496. }
  497. }
  498. else if ((inputSnapshot.m_channelId == AzFramework::InputDeviceKeyboard::Key::AlphanumericA) &&
  499. (static_cast<int>(activeModifierKeys) & static_cast<int>(AzFramework::ModifierKeyMask::CtrlAny)))
  500. {
  501. // Select all
  502. AZStd::string textString;
  503. UiTextBus::EventResult(textString, m_textEntity, &UiTextBus::Events::GetText);
  504. m_textSelectionStartPos = 0;
  505. m_textCursorPos = LyShine::GetUtf8StringLength(textString);
  506. }
  507. else if (command == UiNavigationHelpers::Command::NavHome)
  508. {
  509. // Move cursor to start of text
  510. m_textCursorPos = 0;
  511. if (!isShiftModifierActive)
  512. {
  513. m_textSelectionStartPos = m_textCursorPos;
  514. }
  515. }
  516. else if (command == UiNavigationHelpers::Command::NavEnd)
  517. {
  518. // Move cursor to end of text
  519. AZStd::string textString;
  520. UiTextBus::EventResult(textString, m_textEntity, &UiTextBus::Events::GetText);
  521. m_textCursorPos = LyShine::GetUtf8StringLength(textString);
  522. if (!isShiftModifierActive)
  523. {
  524. m_textSelectionStartPos = m_textCursorPos;
  525. }
  526. }
  527. else if (m_enableClipboard && (inputSnapshot.m_channelId == AzFramework::InputDeviceKeyboard::Key::AlphanumericC) && isLCTRLModifierActive)
  528. {
  529. AZStd::string textString;
  530. UiTextBus::EventResult(textString, m_textEntity, &UiTextBus::Events::GetText);
  531. if (textString.length() > 0 && m_textCursorPos != m_textSelectionStartPos)
  532. {
  533. int left = min(m_textCursorPos, m_textSelectionStartPos);
  534. int right = max(m_textCursorPos, m_textSelectionStartPos);
  535. UiClipboard::SetText(textString.substr(left, right - left));
  536. }
  537. }
  538. else if (m_enableClipboard && (inputSnapshot.m_channelId == AzFramework::InputDeviceKeyboard::Key::AlphanumericX) && isLCTRLModifierActive)
  539. {
  540. AZStd::string textString;
  541. UiTextBus::EventResult(textString, m_textEntity, &UiTextBus::Events::GetText);
  542. if (textString.length() > 0 && m_textCursorPos != m_textSelectionStartPos)
  543. {
  544. int left = min(m_textCursorPos, m_textSelectionStartPos);
  545. int right = max(m_textCursorPos, m_textSelectionStartPos);
  546. UiClipboard::SetText(textString.substr(left, right - left));
  547. textString.erase(left, right - left);
  548. m_textCursorPos = m_textSelectionStartPos = left;
  549. ChangeText(textString);
  550. ResetCursorBlink();
  551. }
  552. }
  553. else if (m_enableClipboard && (inputSnapshot.m_channelId == AzFramework::InputDeviceKeyboard::Key::AlphanumericV) && isLCTRLModifierActive)
  554. {
  555. auto clipboardText = UiClipboard::GetText();
  556. if (clipboardText.length() > 0)
  557. {
  558. AZStd::string textString;
  559. UiTextBus::EventResult(textString, m_textEntity, &UiTextBus::Events::GetText);
  560. // If a range is selected then erase that first
  561. if (m_textCursorPos != m_textSelectionStartPos)
  562. {
  563. int left = min(m_textCursorPos, m_textSelectionStartPos);
  564. int right = max(m_textCursorPos, m_textSelectionStartPos);
  565. textString.erase(left, right - left);
  566. m_textCursorPos = m_textSelectionStartPos = left;
  567. }
  568. // Append text from clipboard
  569. textString.insert(m_textCursorPos, clipboardText);
  570. m_textCursorPos += static_cast<int>(clipboardText.length());
  571. m_textSelectionStartPos = m_textCursorPos;
  572. // If max length is set, remove extra characters
  573. if (m_maxStringLength >= 0 && textString.length() > m_maxStringLength)
  574. {
  575. textString.resize(m_maxStringLength);
  576. }
  577. ChangeText(textString);
  578. ResetCursorBlink();
  579. }
  580. }
  581. else
  582. {
  583. result = false;
  584. }
  585. if (m_textCursorPos != oldTextCursorPos || m_textSelectionStartPos != oldTextSelectionStartPos)
  586. {
  587. AZ::Color color = (m_textSelectionStartPos == m_textCursorPos) ? m_textCursorColor : m_textSelectionColor;
  588. UiTextBus::Event(m_textEntity, &UiTextBus::Events::SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, color);
  589. if (m_textSelectionStartPos == m_textCursorPos)
  590. {
  591. ResetCursorBlink();
  592. }
  593. UiTextBus::Event(m_textEntity, &UiTextBus::Events::ResetCursorLineHint);
  594. }
  595. return result;
  596. }
  597. ////////////////////////////////////////////////////////////////////////////////////////////////////
  598. void UiTextInputComponent::InputPositionUpdate(AZ::Vector2 point)
  599. {
  600. // support dragging to select text, but also support being in a parent draggable
  601. if (m_isPressed)
  602. {
  603. // if we are not yet in the dragging state do some tests to see if we should be
  604. if (!m_isDragging)
  605. {
  606. CheckForDragOrHandOffToParent(point);
  607. }
  608. if (m_isDragging)
  609. {
  610. UiTextBus::EventResult(m_textCursorPos, m_textEntity, &UiTextBus::Events::GetCharIndexFromPoint, point, false);
  611. AZ::Color color = (m_textSelectionStartPos == m_textCursorPos) ? m_textCursorColor : m_textSelectionColor;
  612. UiTextBus::Event(m_textEntity, &UiTextBus::Events::SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, color);
  613. }
  614. }
  615. }
  616. ////////////////////////////////////////////////////////////////////////////////////////////////////
  617. void UiTextInputComponent::LostActiveStatus()
  618. {
  619. UiInteractableComponent::LostActiveStatus();
  620. EndEditState();
  621. }
  622. ////////////////////////////////////////////////////////////////////////////////////////////////////
  623. void UiTextInputComponent::Update(float deltaTime)
  624. {
  625. UiInteractableComponent::Update(deltaTime);
  626. // if we have not set the enable/disable status of the text and placeholder text since
  627. // our status changed then set it
  628. if (m_childTextStateDirtyFlag)
  629. {
  630. bool displayPlaceHolder = true;
  631. if (m_isEditing)
  632. {
  633. displayPlaceHolder = false;
  634. }
  635. else
  636. {
  637. AZStd::string text;
  638. UiTextBus::EventResult(text, m_textEntity, &UiTextBus::Events::GetText);
  639. if (!text.empty())
  640. {
  641. displayPlaceHolder = false;
  642. }
  643. }
  644. UiElementBus::Event(m_placeHolderTextEntity, &UiElementBus::Events::SetIsEnabled, displayPlaceHolder);
  645. UiElementBus::Event(m_textEntity, &UiElementBus::Events::SetIsEnabled, !displayPlaceHolder);
  646. m_childTextStateDirtyFlag = false;
  647. }
  648. // update cursor blinking, only if: this component is active, and blink interval set, and there is no text selection
  649. if (m_isEditing && m_cursorBlinkInterval > 0.0f && m_textSelectionStartPos == m_textCursorPos)
  650. {
  651. const AZ::TimeMs realTimeMs = AZ::GetRealElapsedTimeMs();
  652. const float currentTime = AZ::TimeMsToSeconds(realTimeMs);
  653. if (m_cursorBlinkStartTime == 0.0f)
  654. {
  655. m_cursorBlinkStartTime = currentTime;
  656. }
  657. else if (currentTime - m_cursorBlinkStartTime > m_cursorBlinkInterval * 0.5f)
  658. {
  659. m_textCursorColor.SetA(m_textCursorColor.GetA() ? 0.0f : 1.0f);
  660. m_cursorBlinkStartTime = currentTime;
  661. UiTextBus::Event(
  662. m_textEntity, &UiTextBus::Events::SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, m_textCursorColor);
  663. }
  664. }
  665. }
  666. ////////////////////////////////////////////////////////////////////////////////////////////////////
  667. void UiTextInputComponent::InGamePostActivate()
  668. {
  669. UpdateDisplayedTextFunction();
  670. if (m_clipInputText)
  671. {
  672. UiTextBus::Event(m_textEntity, &UiTextBus::Events::SetOverflowMode, UiTextInterface::OverflowMode::ClipText);
  673. }
  674. }
  675. ////////////////////////////////////////////////////////////////////////////////////////////////////
  676. bool UiTextInputComponent::GetIsPasswordField()
  677. {
  678. return m_isPasswordField;
  679. }
  680. ////////////////////////////////////////////////////////////////////////////////////////////////////
  681. void UiTextInputComponent::SetIsPasswordField(bool passwordField)
  682. {
  683. m_isPasswordField = passwordField;
  684. UpdateDisplayedTextFunction();
  685. }
  686. ////////////////////////////////////////////////////////////////////////////////////////////////////
  687. uint32_t UiTextInputComponent::GetReplacementCharacter()
  688. {
  689. // We store our replacement character as a string due to a reflection issue
  690. // with chars in the editor, so as a workaround we only deal with the first
  691. // character of the string.
  692. return m_replacementCharacter;
  693. }
  694. ////////////////////////////////////////////////////////////////////////////////////////////////////
  695. void UiTextInputComponent::SetReplacementCharacter(uint32_t replacementChar)
  696. {
  697. m_replacementCharacter = replacementChar;
  698. }
  699. ////////////////////////////////////////////////////////////////////////////////////////////////////
  700. AZ::Color UiTextInputComponent::GetTextSelectionColor()
  701. {
  702. return m_textSelectionColor;
  703. }
  704. ////////////////////////////////////////////////////////////////////////////////////////////////////
  705. void UiTextInputComponent::SetTextSelectionColor(const AZ::Color& color)
  706. {
  707. m_textSelectionColor = color;
  708. }
  709. ////////////////////////////////////////////////////////////////////////////////////////////////////
  710. AZ::Color UiTextInputComponent::GetTextCursorColor()
  711. {
  712. return m_textCursorColor;
  713. }
  714. ////////////////////////////////////////////////////////////////////////////////////////////////////
  715. void UiTextInputComponent::SetTextCursorColor(const AZ::Color& color)
  716. {
  717. m_textCursorColor = color;
  718. }
  719. ////////////////////////////////////////////////////////////////////////////////////////////////////
  720. float UiTextInputComponent::GetCursorBlinkInterval()
  721. {
  722. return m_cursorBlinkInterval;
  723. }
  724. ////////////////////////////////////////////////////////////////////////////////////////////////////
  725. void UiTextInputComponent::SetCursorBlinkInterval(float interval)
  726. {
  727. m_cursorBlinkInterval = interval;
  728. }
  729. ////////////////////////////////////////////////////////////////////////////////////////////////////
  730. int UiTextInputComponent::GetMaxStringLength()
  731. {
  732. return m_maxStringLength;
  733. }
  734. ////////////////////////////////////////////////////////////////////////////////////////////////////
  735. void UiTextInputComponent::SetMaxStringLength(int maxCharacters)
  736. {
  737. m_maxStringLength = maxCharacters;
  738. }
  739. ////////////////////////////////////////////////////////////////////////////////////////////////////
  740. UiTextInputComponent::TextInputCallback UiTextInputComponent::GetOnChangeCallback()
  741. {
  742. return m_onChange;
  743. }
  744. ////////////////////////////////////////////////////////////////////////////////////////////////////
  745. void UiTextInputComponent::SetOnChangeCallback(TextInputCallback callbackFunction)
  746. {
  747. m_onChange = callbackFunction;
  748. }
  749. ////////////////////////////////////////////////////////////////////////////////////////////////////
  750. UiTextInputComponent::TextInputCallback UiTextInputComponent::GetOnEndEditCallback()
  751. {
  752. return m_onEndEdit;
  753. }
  754. ////////////////////////////////////////////////////////////////////////////////////////////////////
  755. void UiTextInputComponent::SetOnEndEditCallback(TextInputCallback callbackFunction)
  756. {
  757. m_onEndEdit = callbackFunction;
  758. }
  759. ////////////////////////////////////////////////////////////////////////////////////////////////////
  760. UiTextInputComponent::TextInputCallback UiTextInputComponent::GetOnEnterCallback()
  761. {
  762. return m_onEnter;
  763. }
  764. ////////////////////////////////////////////////////////////////////////////////////////////////////
  765. void UiTextInputComponent::SetOnEnterCallback(TextInputCallback callbackFunction)
  766. {
  767. m_onEnter = callbackFunction;
  768. }
  769. ////////////////////////////////////////////////////////////////////////////////////////////////////
  770. const LyShine::ActionName& UiTextInputComponent::GetChangeAction()
  771. {
  772. return m_changeAction;
  773. }
  774. ////////////////////////////////////////////////////////////////////////////////////////////////////
  775. void UiTextInputComponent::SetChangeAction(const LyShine::ActionName& actionName)
  776. {
  777. m_changeAction = actionName;
  778. }
  779. ////////////////////////////////////////////////////////////////////////////////////////////////////
  780. const LyShine::ActionName& UiTextInputComponent::GetEndEditAction()
  781. {
  782. return m_endEditAction;
  783. }
  784. ////////////////////////////////////////////////////////////////////////////////////////////////////
  785. void UiTextInputComponent::SetEndEditAction(const LyShine::ActionName& actionName)
  786. {
  787. m_endEditAction = actionName;
  788. }
  789. ////////////////////////////////////////////////////////////////////////////////////////////////////
  790. const LyShine::ActionName& UiTextInputComponent::GetEnterAction()
  791. {
  792. return m_enterAction;
  793. }
  794. ////////////////////////////////////////////////////////////////////////////////////////////////////
  795. void UiTextInputComponent::SetEnterAction(const LyShine::ActionName& actionName)
  796. {
  797. m_enterAction = actionName;
  798. }
  799. ////////////////////////////////////////////////////////////////////////////////////////////////////
  800. AZ::EntityId UiTextInputComponent::GetTextEntity()
  801. {
  802. return m_textEntity;
  803. }
  804. ////////////////////////////////////////////////////////////////////////////////////////////////////
  805. void UiTextInputComponent::SetTextEntity(AZ::EntityId textEntity)
  806. {
  807. m_textEntity = textEntity;
  808. m_childTextStateDirtyFlag = true;
  809. UpdateDisplayedTextFunction();
  810. }
  811. ////////////////////////////////////////////////////////////////////////////////////////////////////
  812. AZStd::string UiTextInputComponent::GetText()
  813. {
  814. AZStd::string text;
  815. UiTextBus::EventResult(text, m_textEntity, &UiTextBus::Events::GetText);
  816. return text;
  817. }
  818. ////////////////////////////////////////////////////////////////////////////////////////////////////
  819. void UiTextInputComponent::SetText(const AZStd::string& text)
  820. {
  821. UiTextBus::Event(m_textEntity, &UiTextBus::Events::SetText, text);
  822. m_childTextStateDirtyFlag = true;
  823. // Make sure cursor position and selection is in range
  824. if (m_textCursorPos >= 0)
  825. {
  826. int maxPos = LyShine::GetUtf8StringLength(text);
  827. int newTextCursorPos = AZ::GetMin(m_textCursorPos, maxPos);
  828. int newTextSelectionStartPos = AZ::GetMin(m_textSelectionStartPos, maxPos);
  829. if (newTextCursorPos != m_textCursorPos || newTextSelectionStartPos != m_textSelectionStartPos)
  830. {
  831. m_textCursorPos = newTextCursorPos;
  832. m_textSelectionStartPos = newTextSelectionStartPos;
  833. int selStartIndex, selEndIndex;
  834. UiTextBus::Event(m_textEntity, &UiTextBus::Events::GetSelectionRange, selStartIndex, selEndIndex);
  835. if (selStartIndex >= 0)
  836. {
  837. UiTextBus::Event(
  838. m_textEntity, &UiTextBus::Events::SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, m_textCursorColor);
  839. }
  840. }
  841. }
  842. }
  843. ////////////////////////////////////////////////////////////////////////////////////////////////////
  844. AZ::EntityId UiTextInputComponent::GetPlaceHolderTextEntity()
  845. {
  846. return m_placeHolderTextEntity;
  847. }
  848. ////////////////////////////////////////////////////////////////////////////////////////////////////
  849. void UiTextInputComponent::SetPlaceHolderTextEntity(AZ::EntityId textEntity)
  850. {
  851. m_placeHolderTextEntity = textEntity;
  852. m_childTextStateDirtyFlag = true;
  853. }
  854. ////////////////////////////////////////////////////////////////////////////////////////////////////
  855. bool UiTextInputComponent::GetIsClipboardEnabled()
  856. {
  857. return m_enableClipboard;
  858. }
  859. ////////////////////////////////////////////////////////////////////////////////////////////////////
  860. void UiTextInputComponent::SetIsClipboardEnabled(bool enableClipboard)
  861. {
  862. m_enableClipboard = enableClipboard;
  863. }
  864. ////////////////////////////////////////////////////////////////////////////////////////////////////
  865. // PROTECTED MEMBER FUNCTIONS
  866. ////////////////////////////////////////////////////////////////////////////////////////////////////
  867. ////////////////////////////////////////////////////////////////////////////////////////////////////
  868. void UiTextInputComponent::Activate()
  869. {
  870. UiInteractableComponent::Activate();
  871. UiInitializationBus::Handler::BusConnect(m_entity->GetId());
  872. UiTextInputBus::Handler::BusConnect(m_entity->GetId());
  873. }
  874. ////////////////////////////////////////////////////////////////////////////////////////////////////
  875. void UiTextInputComponent::Deactivate()
  876. {
  877. UiInteractableComponent::Deactivate();
  878. UiInitializationBus::Handler::BusDisconnect();
  879. UiTextInputBus::Handler::BusDisconnect();
  880. }
  881. ////////////////////////////////////////////////////////////////////////////////////////////////////
  882. bool UiTextInputComponent::IsAutoActivationSupported()
  883. {
  884. return true;
  885. }
  886. ////////////////////////////////////////////////////////////////////////////////////////////////////
  887. void UiTextInputComponent::BeginEditState()
  888. {
  889. m_isEditing = true;
  890. // force re-evaluation of whether text or placeholder text should be displayed
  891. m_childTextStateDirtyFlag = true;
  892. // position the cursor in the text entity
  893. UiTextBus::Event(m_textEntity, &UiTextBus::Events::SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, m_textCursorColor);
  894. ResetCursorBlink();
  895. }
  896. ////////////////////////////////////////////////////////////////////////////////////////////////////
  897. void UiTextInputComponent::EndEditState()
  898. {
  899. AZStd::string textString;
  900. UiTextBus::EventResult(textString, m_textEntity, &UiTextBus::Events::GetText);
  901. // if a C++ callback is registered for OnEndEdit then call it
  902. if (m_onEndEdit)
  903. {
  904. // pass the entered text string to the C++ callback
  905. m_onEndEdit(GetEntityId(), textString);
  906. }
  907. // Tell any action listeners that the edit ended
  908. if (!m_endEditAction.empty())
  909. {
  910. // canvas listeners will get the action name (e.g. something like "EmailEntered") plus
  911. // the ID of this entity.
  912. AZ::EntityId canvasEntityId;
  913. UiElementBus::EventResult(canvasEntityId, GetEntityId(), &UiElementBus::Events::GetCanvasEntityId);
  914. UiCanvasNotificationBus::Event(canvasEntityId, &UiCanvasNotificationBus::Events::OnAction, GetEntityId(), m_endEditAction);
  915. }
  916. UiTextInputNotificationBus::Event(GetEntityId(), &UiTextInputNotificationBus::Events::OnTextInputEndEdit, textString);
  917. // clear the selection highlight
  918. UiTextBus::Event(m_textEntity, &UiTextBus::Events::ClearSelectionRange);
  919. m_textCursorPos = m_textSelectionStartPos = -1;
  920. if (m_isTextInputStarted)
  921. {
  922. AzFramework::InputTextEntryRequestBus::Broadcast(&AzFramework::InputTextEntryRequests::TextEntryStop);
  923. m_isTextInputStarted = false;
  924. }
  925. m_isEditing = false;
  926. // force re-evaluation of whether text or placeholder text should be displayed
  927. m_childTextStateDirtyFlag = true;
  928. }
  929. ////////////////////////////////////////////////////////////////////////////////////////////////////
  930. // calculate how much we have dragged along the text
  931. float UiTextInputComponent::GetValidDragDistanceInPixels(AZ::Vector2 startPoint, AZ::Vector2 endPoint)
  932. {
  933. const float validDragRatio = 0.5f;
  934. // convert the drag vector to local space
  935. AZ::Matrix4x4 transformFromViewport;
  936. UiTransformBus::Event(GetEntityId(), &UiTransformBus::Events::GetTransformFromViewport, transformFromViewport);
  937. AZ::Vector2 dragVec = endPoint - startPoint;
  938. AZ::Vector3 dragVec3(dragVec.GetX(), dragVec.GetY(), 0.0f);
  939. AZ::Vector3 localDragVec = transformFromViewport.Multiply3x3(dragVec3);
  940. // the text input component only supports drag along the x axis so zero the y axis
  941. localDragVec.SetY(0.0f);
  942. // convert back to viewport space
  943. AZ::Matrix4x4 transformToViewport;
  944. UiTransformBus::Event(GetEntityId(), &UiTransformBus::Events::GetTransformToViewport, transformToViewport);
  945. AZ::Vector3 validDragVec = transformToViewport.Multiply3x3(localDragVec);
  946. float validDistance = validDragVec.GetLengthSq();
  947. float totalDistance = dragVec.GetLengthSq();
  948. // if they are not dragging mostly in a valid direction then ignore the drag
  949. if (validDistance / totalDistance < validDragRatio)
  950. {
  951. validDistance = 0.0f;
  952. }
  953. // return the valid drag distance
  954. return validDistance;
  955. }
  956. ////////////////////////////////////////////////////////////////////////////////////////////////////
  957. void UiTextInputComponent::CheckForDragOrHandOffToParent(AZ::Vector2 point)
  958. {
  959. AZ::EntityId parentDraggable;
  960. UiElementBus::EventResult(parentDraggable, GetEntityId(), &UiElementBus::Events::FindParentInteractableSupportingDrag, point);
  961. // if this interactable is inside another interactable that supports drag then we use
  962. // a threshold value before starting a drag on this interactable
  963. const float normalDragThreshold = 0.0f;
  964. const float containedDragThreshold = 5.0f;
  965. float dragThreshold = normalDragThreshold;
  966. if (parentDraggable.IsValid())
  967. {
  968. dragThreshold = containedDragThreshold;
  969. }
  970. // calculate how much we have dragged along the axis of the slider
  971. float validDragDistance = GetValidDragDistanceInPixels(m_pressedPoint, point);
  972. // only enter drag mode if we dragged above the threshold AND there is something to select
  973. AZStd::string textString;
  974. UiTextBus::EventResult(textString, m_textEntity, &UiTextBus::Events::GetText);
  975. if (validDragDistance > dragThreshold && !textString.empty())
  976. {
  977. // we dragged above the threshold value along axis of slider
  978. m_isDragging = true;
  979. // enter editing state if we are not already in it
  980. if (!m_isEditing)
  981. {
  982. BeginEditState();
  983. }
  984. }
  985. else if (parentDraggable.IsValid())
  986. {
  987. // offer the parent draggable the chance to become the active interactable
  988. bool handOff = false;
  989. UiInteractableBus::EventResult(
  990. handOff,
  991. parentDraggable,
  992. &UiInteractableBus::Events::OfferDragHandOff,
  993. GetEntityId(),
  994. m_pressedPoint,
  995. point,
  996. containedDragThreshold);
  997. if (handOff)
  998. {
  999. // interaction has been handed off to a container entity
  1000. m_isPressed = false;
  1001. EndEditState();
  1002. }
  1003. }
  1004. }
  1005. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1006. void UiTextInputComponent::OnReplacementCharacterChange()
  1007. {
  1008. if (m_replacementCharacter == '\0')
  1009. {
  1010. m_replacementCharacter = defaultReplacementChar;
  1011. }
  1012. }
  1013. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1014. void UiTextInputComponent::UpdateDisplayedTextFunction()
  1015. {
  1016. // If we're a password input box then we need to set up a callback to allow us to change how
  1017. // the text stored in our child component is displayed before rendering.
  1018. if (m_isPasswordField)
  1019. {
  1020. // Use a lambda here so we can easily access our instance to retrieve the
  1021. // currently configured replacement character
  1022. UiTextBus::Event(
  1023. m_textEntity,
  1024. &UiTextBus::Events::SetDisplayedTextFunction,
  1025. [this](const AZStd::string& originalText)
  1026. {
  1027. // NOTE: this assumes the uint32_t can be interpreted as a wchar_t, it seems to
  1028. // work for cases tested but may not in general.
  1029. wchar_t wcharString[2] = { static_cast<wchar_t>(this->GetReplacementCharacter()), 0 };
  1030. AZStd::string replacementCharString;
  1031. AZStd::to_string(replacementCharString, { wcharString, 1 });
  1032. int numReplacementChars = LyShine::GetUtf8StringLength(originalText);
  1033. AZStd::string replacedString;
  1034. replacedString.reserve(numReplacementChars * replacementCharString.length());
  1035. for (int i = 0; i < numReplacementChars; i++)
  1036. {
  1037. replacedString += replacementCharString;
  1038. }
  1039. return replacedString;
  1040. });
  1041. }
  1042. else
  1043. {
  1044. UiTextBus::Event(m_textEntity, &UiTextBus::Events::SetDisplayedTextFunction, nullptr);
  1045. }
  1046. }
  1047. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1048. UiTextInputComponent::EntityComboBoxVec UiTextInputComponent::PopulateTextEntityList()
  1049. {
  1050. EntityComboBoxVec result;
  1051. AZStd::vector<AZ::EntityId> entityIdList;
  1052. // add a first entry for "None"
  1053. result.push_back(AZStd::make_pair(AZ::EntityId(AZ::EntityId()), "<None>"));
  1054. // allow the destination to be the same entity as the source by
  1055. // adding this entity (if it has a text component)
  1056. if (UiTextBus::FindFirstHandler(GetEntityId()))
  1057. {
  1058. result.push_back(AZStd::make_pair(AZ::EntityId(GetEntityId()), GetEntity()->GetName()));
  1059. }
  1060. // Add all descendant elements that have Text components to the lists
  1061. AddDescendantTextElements(GetEntityId(), result);
  1062. return result;
  1063. }
  1064. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1065. UiInteractableStatesInterface::State UiTextInputComponent::ComputeInteractableState()
  1066. {
  1067. // This currently happens every frame. Needs optimization to just happen on events
  1068. UiInteractableStatesInterface::State state = UiInteractableStatesInterface::StateNormal;
  1069. if (!m_isHandlingEvents)
  1070. {
  1071. // not handling events, use disabled state
  1072. state = UiInteractableStatesInterface::StateDisabled;
  1073. }
  1074. else if (m_isPressed && m_isHover)
  1075. {
  1076. // We only use the pressed state when the state is pressed AND the mouse is over the rect
  1077. state = UiInteractableStatesInterface::StatePressed;
  1078. }
  1079. else if (m_isHover || m_isPressed || m_isEditing)
  1080. {
  1081. // we use the hover state for normal hover but also if the state is pressed but
  1082. // the mouse is outside the rect, and also if the text is being edited
  1083. state = UiInteractableStatesInterface::StateHover;
  1084. }
  1085. return state;
  1086. }
  1087. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1088. // PROTECTED STATIC MEMBER FUNCTIONS
  1089. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1090. void UiTextInputComponent::Reflect(AZ::ReflectContext* context)
  1091. {
  1092. AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
  1093. if (serializeContext)
  1094. {
  1095. serializeContext->Class<UiTextInputComponent, UiInteractableComponent>()
  1096. ->Version(8, &VersionConverter)
  1097. // Elements group
  1098. ->Field("Text", &UiTextInputComponent::m_textEntity)
  1099. ->Field("PlaceHolderText", &UiTextInputComponent::m_placeHolderTextEntity)
  1100. // Text editing group
  1101. ->Field("TextSelectionColor", &UiTextInputComponent::m_textSelectionColor)
  1102. ->Field("TextCursorColor", &UiTextInputComponent::m_textCursorColor)
  1103. ->Field("MaxStringLength", &UiTextInputComponent::m_maxStringLength)
  1104. ->Field("CursorBlinkInterval", &UiTextInputComponent::m_cursorBlinkInterval)
  1105. ->Field("IsPasswordField", &UiTextInputComponent::m_isPasswordField)
  1106. ->Field("ReplacementCharacter", &UiTextInputComponent::m_replacementCharacter)
  1107. ->Field("ClipInputText", &UiTextInputComponent::m_clipInputText)
  1108. ->Field("EnableClipboard", &UiTextInputComponent::m_enableClipboard)
  1109. // Actions group
  1110. ->Field("ChangeAction", &UiTextInputComponent::m_changeAction)
  1111. ->Field("EndEditAction", &UiTextInputComponent::m_endEditAction)
  1112. ->Field("EnterAction", &UiTextInputComponent::m_enterAction);
  1113. AZ::EditContext* ec = serializeContext->GetEditContext();
  1114. if (ec)
  1115. {
  1116. auto editInfo = ec->Class<UiTextInputComponent>("TextInput", "An interactable component for editing a text string.");
  1117. editInfo->ClassElement(AZ::Edit::ClassElements::EditorData, "")
  1118. ->Attribute(AZ::Edit::Attributes::Category, "UI")
  1119. ->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/UiTextInput.png")
  1120. ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/UiTextInput.png")
  1121. ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("UI"))
  1122. ->Attribute(AZ::Edit::Attributes::AutoExpand, true);
  1123. // Elements group
  1124. {
  1125. editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Elements")
  1126. ->Attribute(AZ::Edit::Attributes::AutoExpand, true);
  1127. editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiTextInputComponent::m_textEntity, "Text", "The UI element to hold the entered text.")
  1128. ->Attribute(AZ::Edit::Attributes::EnumValues, &UiTextInputComponent::PopulateTextEntityList);
  1129. editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiTextInputComponent::m_placeHolderTextEntity, "Placeholder text", "The UI element to display the placeholder text.")
  1130. ->Attribute(AZ::Edit::Attributes::EnumValues, &UiTextInputComponent::PopulateTextEntityList);
  1131. }
  1132. // Text Editing group
  1133. {
  1134. editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Text editing")
  1135. ->Attribute(AZ::Edit::Attributes::AutoExpand, true);
  1136. editInfo->DataElement(AZ::Edit::UIHandlers::Color, &UiTextInputComponent::m_textSelectionColor,
  1137. "Selection color", "The text selection color.");
  1138. editInfo->DataElement(AZ::Edit::UIHandlers::Color, &UiTextInputComponent::m_textCursorColor,
  1139. "Cursor color", "The text cursor color.");
  1140. editInfo->DataElement(AZ::Edit::UIHandlers::SpinBox, &UiTextInputComponent::m_cursorBlinkInterval,
  1141. "Cursor blink time", "The cursor blink interval.")
  1142. ->Attribute(AZ::Edit::Attributes::Min, 0.0f)
  1143. ->Attribute(AZ::Edit::Attributes::Step, 0.1f);
  1144. editInfo->DataElement(AZ::Edit::UIHandlers::SpinBox, &UiTextInputComponent::m_maxStringLength,
  1145. "Max char count", "The maximum string length that can be entered. For unlimited enter -1.")
  1146. ->Attribute(AZ::Edit::Attributes::Min, -1)
  1147. ->Attribute(AZ::Edit::Attributes::Step, 1);
  1148. editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiTextInputComponent::m_isPasswordField,
  1149. "Is password field", "A password field hides the entered text.")
  1150. ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC_CE("RefreshEntireTree"));
  1151. editInfo->DataElement(AZ_CRC_CE("Char"), &UiTextInputComponent::m_replacementCharacter,
  1152. "Replacement character", "The replacement character used to hide password text.")
  1153. ->Attribute(AZ::Edit::Attributes::ChangeNotify, &UiTextInputComponent::OnReplacementCharacterChange)
  1154. ->Attribute(AZ::Edit::Attributes::Visibility, &UiTextInputComponent::GetIsPasswordField);
  1155. editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiTextInputComponent::m_clipInputText,
  1156. "Clip input text", "When checked, the input text is clipped to this element's rect.");
  1157. editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiTextInputComponent::m_enableClipboard,
  1158. "Enable clipboard", "When checked, Ctrl-C, Ctrl-X, and Ctrl-V events will be handled");
  1159. }
  1160. // Actions group
  1161. {
  1162. editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Actions")
  1163. ->Attribute(AZ::Edit::Attributes::AutoExpand, true);
  1164. editInfo->DataElement(0, &UiTextInputComponent::m_changeAction, "Change", "The action name triggered on each character typed.");
  1165. editInfo->DataElement(0, &UiTextInputComponent::m_endEditAction, "End edit", "The action name triggered on either focus change or enter.");
  1166. editInfo->DataElement(0, &UiTextInputComponent::m_enterAction, "Enter", "The action name triggered when enter is pressed.");
  1167. }
  1168. }
  1169. }
  1170. AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context);
  1171. if (behaviorContext)
  1172. {
  1173. behaviorContext->EBus<UiTextInputBus>("UiTextInputBus")
  1174. ->Event("GetTextSelectionColor", &UiTextInputBus::Events::GetTextSelectionColor)
  1175. ->Event("SetTextSelectionColor", &UiTextInputBus::Events::SetTextSelectionColor)
  1176. ->Event("GetTextCursorColor", &UiTextInputBus::Events::GetTextCursorColor)
  1177. ->Event("SetTextCursorColor", &UiTextInputBus::Events::SetTextCursorColor)
  1178. ->Event("GetCursorBlinkInterval", &UiTextInputBus::Events::GetCursorBlinkInterval)
  1179. ->Event("SetCursorBlinkInterval", &UiTextInputBus::Events::SetCursorBlinkInterval)
  1180. ->Event("GetMaxStringLength", &UiTextInputBus::Events::GetMaxStringLength)
  1181. ->Event("SetMaxStringLength", &UiTextInputBus::Events::SetMaxStringLength)
  1182. ->Event("GetChangeAction", &UiTextInputBus::Events::GetChangeAction)
  1183. ->Event("SetChangeAction", &UiTextInputBus::Events::SetChangeAction)
  1184. ->Event("GetEndEditAction", &UiTextInputBus::Events::GetEndEditAction)
  1185. ->Event("SetEndEditAction", &UiTextInputBus::Events::SetEndEditAction)
  1186. ->Event("GetEnterAction", &UiTextInputBus::Events::GetEnterAction)
  1187. ->Event("SetEnterAction", &UiTextInputBus::Events::SetEnterAction)
  1188. ->Event("GetTextEntity", &UiTextInputBus::Events::GetTextEntity)
  1189. ->Event("SetTextEntity", &UiTextInputBus::Events::SetTextEntity)
  1190. ->Event("GetText", &UiTextInputBus::Events::GetText)
  1191. ->Event("SetText", &UiTextInputBus::Events::SetText)
  1192. ->Event("GetPlaceHolderTextEntity", &UiTextInputBus::Events::GetPlaceHolderTextEntity)
  1193. ->Event("SetPlaceHolderTextEntity", &UiTextInputBus::Events::SetPlaceHolderTextEntity)
  1194. ->Event("GetIsPasswordField", &UiTextInputBus::Events::GetIsPasswordField)
  1195. ->Event("SetIsPasswordField", &UiTextInputBus::Events::SetIsPasswordField)
  1196. ->Event("GetReplacementCharacter", &UiTextInputBus::Events::GetReplacementCharacter)
  1197. ->Event("SetReplacementCharacter", &UiTextInputBus::Events::SetReplacementCharacter)
  1198. ->Event("GetIsClipboardEnabled", &UiTextInputBus::Events::GetIsClipboardEnabled)
  1199. ->Event("SetIsClipboardEnabled", &UiTextInputBus::Events::SetIsClipboardEnabled)
  1200. ->VirtualProperty("TextSelectionColor", "GetTextSelectionColor", "SetTextSelectionColor")
  1201. ->VirtualProperty("TextCursorColor", "GetTextCursorColor", "SetTextCursorColor")
  1202. ->VirtualProperty("CursorBlinkInterval", "GetCursorBlinkInterval", "SetCursorBlinkInterval")
  1203. ->VirtualProperty("MaxStringLength", "GetMaxStringLength", "SetMaxStringLength");
  1204. behaviorContext->Class<UiTextInputComponent>()->RequestBus("UiTextInputBus");
  1205. behaviorContext->EBus<UiTextInputNotificationBus>("UiTextInputNotificationBus")
  1206. ->Handler<BehaviorUiTextInputNotificationBusHandler>();
  1207. }
  1208. }
  1209. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1210. // PRIVATE MEMBER FUNCTIONS
  1211. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1212. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1213. void UiTextInputComponent::ChangeText(const AZStd::string& textString)
  1214. {
  1215. // For user-inputted text, we assume that users don't want to input
  1216. // text as styling markup (but rather plain-text).
  1217. UiTextBus::Event(m_textEntity, &UiTextBus::Events::SetTextWithFlags, textString, UiTextInterface::SetEscapeMarkup);
  1218. // if a C++ callback is registered for OnChange then call it
  1219. if (m_onChange)
  1220. {
  1221. // pass the entered text string to the C++ callback
  1222. m_onChange(GetEntityId(), textString);
  1223. }
  1224. // Tell any action listeners about the event
  1225. if (!m_changeAction.empty())
  1226. {
  1227. // canvas listeners will get the action name (e.g. something like "EmailEdited") plus
  1228. // the ID of this entity.
  1229. AZ::EntityId canvasEntityId;
  1230. UiElementBus::EventResult(canvasEntityId, GetEntityId(), &UiElementBus::Events::GetCanvasEntityId);
  1231. UiCanvasNotificationBus::Event(canvasEntityId, &UiCanvasNotificationBus::Events::OnAction, GetEntityId(), m_changeAction);
  1232. }
  1233. UiTextInputNotificationBus::Event(GetEntityId(), &UiTextInputNotificationBus::Events::OnTextInputChange, textString);
  1234. }
  1235. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1236. void UiTextInputComponent::ResetCursorBlink()
  1237. {
  1238. m_textCursorColor.SetA(1.0f);
  1239. m_cursorBlinkStartTime = 0.0f;
  1240. UiTextBus::Event(m_textEntity, &UiTextBus::Events::SetSelectionRange, m_textSelectionStartPos, m_textCursorPos, m_textCursorColor);
  1241. }
  1242. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1243. void UiTextInputComponent::CheckStartTextInput()
  1244. {
  1245. // We do not bring up the on-screen keyboard when a drag is started, only on a "click" or at
  1246. // the end of a drag. But a drag begin can cause BeginEditState to be called. So we can begin
  1247. // edit state before we bring up the on-screen keyboard. So here we test if it is time to bring
  1248. // up the keyboard.
  1249. if (m_isEditing && !m_isTextInputStarted)
  1250. {
  1251. // ensure the on-screen keyboard is shown on mobile and console platforms
  1252. AzFramework::InputTextEntryRequests::VirtualKeyboardOptions options;
  1253. AZStd::string textString;
  1254. UiTextBus::EventResult(textString, m_textEntity, &UiTextBus::Events::GetText);
  1255. options.m_initialText = Utf8SubString(textString, m_textCursorPos, m_textSelectionStartPos);
  1256. AZ::EntityId canvasEntityId;
  1257. UiElementBus::EventResult(canvasEntityId, GetEntityId(), &UiElementBus::Events::GetCanvasEntityId);
  1258. // Calculate height available for virtual keyboard. In game mode, canvas size is the same as viewport size
  1259. AZ::Vector2 canvasSize;
  1260. UiCanvasBus::EventResult(canvasSize, canvasEntityId, &UiCanvasBus::Events::GetCanvasSize);
  1261. UiTransformInterface::RectPoints rectPoints;
  1262. UiTransformBus::Event(GetEntityId(), &UiTransformBus::Events::GetViewportSpacePoints, rectPoints);
  1263. const AZ::Vector2 bottomRight = rectPoints.GetAxisAlignedBottomRight();
  1264. options.m_normalizedMinY = (canvasSize.GetY() > 0.0f) ? bottomRight.GetY() / canvasSize.GetY() : 0.0f;
  1265. UiCanvasBus::EventResult(options.m_localUserId, canvasEntityId, &UiCanvasBus::Events::GetLocalUserIdInputFilter);
  1266. AzFramework::InputTextEntryRequestBus::Broadcast(&AzFramework::InputTextEntryRequests::TextEntryStart, options);
  1267. m_isTextInputStarted = true;
  1268. }
  1269. }
  1270. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1271. // PRIVATE STATIC MEMBER FUNCTIONS
  1272. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1273. ////////////////////////////////////////////////////////////////////////////////////////////////////
  1274. bool UiTextInputComponent::VersionConverter(AZ::SerializeContext& context,
  1275. AZ::SerializeContext::DataElementNode& classElement)
  1276. {
  1277. // conversion from version 1:
  1278. // - Need to convert CryString elements to AZStd::string
  1279. // - Need to convert Color to Color and Alpha
  1280. // conversion from version 1 or 2 to current:
  1281. // - Need to convert CryString ActionName elements to AZStd::string
  1282. AZ_Assert(classElement.GetVersion() > 2, "Unsupported UiTextInputComponent version: %d", classElement.GetVersion());
  1283. // conversion from version 1, 2 or 3 to current:
  1284. // - Need to convert AZStd::string sprites to AzFramework::SimpleAssetReference<LmbrCentral::TextureAsset>
  1285. if (classElement.GetVersion() <= 3)
  1286. {
  1287. if (!LyShine::ConvertSubElementFromAzStringToAssetRef<LmbrCentral::TextureAsset>(context, classElement, "SelectedSprite"))
  1288. {
  1289. return false;
  1290. }
  1291. if (!LyShine::ConvertSubElementFromAzStringToAssetRef<LmbrCentral::TextureAsset>(context, classElement, "PressedSprite"))
  1292. {
  1293. return false;
  1294. }
  1295. }
  1296. // Conversion from version 4 to 5:
  1297. if (classElement.GetVersion() < 5)
  1298. {
  1299. // find the base class (AZ::Component)
  1300. // NOTE: in very old versions there may not be a base class because the base class was not serialized
  1301. int componentBaseClassIndex = classElement.FindElement(AZ_CRC_CE("BaseClass1"));
  1302. // If there was a base class, make a copy and remove it
  1303. AZ::SerializeContext::DataElementNode componentBaseClassNode;
  1304. if (componentBaseClassIndex != -1)
  1305. {
  1306. // make a local copy of the component base class node
  1307. componentBaseClassNode = classElement.GetSubElement(componentBaseClassIndex);
  1308. // remove the component base class from the button
  1309. classElement.RemoveElement(componentBaseClassIndex);
  1310. }
  1311. // Add a new base class (UiInteractableComponent)
  1312. int interactableBaseClassIndex = classElement.AddElement<UiInteractableComponent>(context, "BaseClass1");
  1313. AZ::SerializeContext::DataElementNode& interactableBaseClassNode = classElement.GetSubElement(interactableBaseClassIndex);
  1314. // if there was previously a base class...
  1315. if (componentBaseClassIndex != -1)
  1316. {
  1317. // copy the component base class into the new interactable base class
  1318. // Since AZ::Component is now the base class of UiInteractableComponent
  1319. interactableBaseClassNode.AddElement(componentBaseClassNode);
  1320. }
  1321. // Move the selected/hover state to the base class
  1322. if (!UiSerialize::MoveToInteractableStateActions(context, classElement, "HoverStateActions",
  1323. "SelectedColor", "SelectedAlpha", "SelectedSprite"))
  1324. {
  1325. return false;
  1326. }
  1327. // Move the pressed state to the base class
  1328. if (!UiSerialize::MoveToInteractableStateActions(context, classElement, "PressedStateActions",
  1329. "PressedColor", "PressedAlpha", "PressedSprite"))
  1330. {
  1331. return false;
  1332. }
  1333. }
  1334. // Conversion from version 5 to 6:
  1335. if (classElement.GetVersion() < 6)
  1336. {
  1337. int clipTextIndex = classElement.AddElement<bool>(context, "ClipInputText");
  1338. if (clipTextIndex == -1)
  1339. {
  1340. // Error adding the new sub element
  1341. AZ_Error("Serialization", false, "Failed to create ClipInputText node");
  1342. return false;
  1343. }
  1344. AZ::SerializeContext::DataElementNode& clipTextNode = classElement.GetSubElement(clipTextIndex);
  1345. clipTextNode.SetData(context, false);
  1346. }
  1347. // conversion from version 6 to 7: Need to convert ColorF to AZ::Color
  1348. if (classElement.GetVersion() < 7)
  1349. {
  1350. if (!LyShine::ConvertSubElementFromColorFToAzColor(context, classElement, "TextSelectionColor"))
  1351. {
  1352. return false;
  1353. }
  1354. if (!LyShine::ConvertSubElementFromColorFToAzColor(context, classElement, "TextCursorColor"))
  1355. {
  1356. return false;
  1357. }
  1358. }
  1359. // Conversion from 7 to 8: Need to convert char to uint32_t
  1360. if (classElement.GetVersion() < 8)
  1361. {
  1362. if (!LyShine::ConvertSubElementFromCharToUInt32(context, classElement, "ReplacementCharacter"))
  1363. {
  1364. return false;
  1365. }
  1366. }
  1367. return true;
  1368. }