CTextInput.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. /*
  2. * CTextInput.cpp, part of VCMI engine
  3. *
  4. * Authors: listed in file AUTHORS in main folder
  5. *
  6. * License: GNU General Public License v2.0 or later
  7. * Full text of license available in license.txt file, in main folder
  8. *
  9. */
  10. #include "StdInc.h"
  11. #include "CTextInput.h"
  12. #include "Images.h"
  13. #include "TextControls.h"
  14. #include "../GameEngine.h"
  15. #include "../eventsSDL/InputHandler.h"
  16. #include "../gui/Shortcut.h"
  17. #include "../render/Graphics.h"
  18. #include "../render/IFont.h"
  19. #include "../render/IRenderHandler.h"
  20. #include "../../lib/texts/TextOperations.h"
  21. #include <boost/lexical_cast.hpp>
  22. std::list<CFocusable *> CFocusable::focusables;
  23. CFocusable * CFocusable::inputWithFocus;
  24. CTextInputWithConfirm::CTextInputWithConfirm(const Rect & Pos, EFonts font, ETextAlignment alignment, std::string text, bool limitToRect, std::function<void()> confirmCallback)
  25. : CTextInput(Pos, font, alignment, false), confirmCb(confirmCallback), limitToRect(limitToRect), initialText(text)
  26. {
  27. setText(text);
  28. }
  29. bool CTextInputWithConfirm::captureThisKey(EShortcut key)
  30. {
  31. return hasFocus() && (key == EShortcut::GLOBAL_ACCEPT || key == EShortcut::GLOBAL_CANCEL || key == EShortcut::GLOBAL_BACKSPACE);
  32. }
  33. void CTextInputWithConfirm::keyPressed(EShortcut key)
  34. {
  35. if(!hasFocus())
  36. return;
  37. if(key == EShortcut::GLOBAL_ACCEPT)
  38. confirm();
  39. else if(key == EShortcut::GLOBAL_CANCEL)
  40. {
  41. setText(initialText);
  42. removeFocus();
  43. }
  44. CTextInput::keyPressed(key);
  45. }
  46. bool CTextInputWithConfirm::receiveEvent(const Point & position, int eventType) const
  47. {
  48. return eventType == AEventsReceiver::LCLICK; // capture all left clicks (not only within control)
  49. }
  50. void CTextInputWithConfirm::clickReleased(const Point & cursorPosition)
  51. {
  52. if(!pos.isInside(cursorPosition)) // clicked outside
  53. confirm();
  54. }
  55. void CTextInputWithConfirm::clickPressed(const Point & cursorPosition)
  56. {
  57. if(pos.isInside(cursorPosition)) // clickPressed should respect control area (receiveEvent also affects this)
  58. CTextInput::clickPressed(cursorPosition);
  59. }
  60. void CTextInputWithConfirm::onFocusGot()
  61. {
  62. initialText = getText();
  63. CTextInput::onFocusGot();
  64. }
  65. void CTextInputWithConfirm::textInputted(const std::string & enteredText)
  66. {
  67. if(!hasFocus())
  68. return;
  69. std::string visibleText = getVisibleText() + enteredText;
  70. const auto & font = ENGINE->renderHandler().loadFont(label->font);
  71. if(!limitToRect || (font->getStringWidth(visibleText) - CLabel::getDelimitersWidth(label->font, visibleText)) < pos.w)
  72. CTextInput::textInputted(enteredText);
  73. }
  74. void CTextInputWithConfirm::deactivate()
  75. {
  76. removeUsedEvents(LCLICK);
  77. CTextInput::deactivate();
  78. }
  79. void CTextInputWithConfirm::confirm()
  80. {
  81. if(getText().empty())
  82. setText(initialText);
  83. if(confirmCb && initialText != getText())
  84. confirmCb();
  85. removeFocus();
  86. }
  87. CTextInput::CTextInput(const Rect & Pos)
  88. :originalAlignment(ETextAlignment::CENTERLEFT)
  89. {
  90. pos += Pos.topLeft();
  91. pos.h = Pos.h;
  92. pos.w = Pos.w;
  93. addUsedEvents(LCLICK | SHOW_POPUP | KEYBOARD | TEXTINPUT);
  94. }
  95. void CTextInput::createLabel(bool giveFocusToInput)
  96. {
  97. OBJECT_CONSTRUCTION;
  98. label = std::make_shared<CLabel>();
  99. label->pos = pos;
  100. label->alignment = originalAlignment;
  101. #if !defined(VCMI_MOBILE)
  102. if(giveFocusToInput)
  103. giveFocus();
  104. #endif
  105. }
  106. CTextInput::CTextInput(const Rect & Pos, EFonts font, ETextAlignment alignment, bool giveFocusToInput)
  107. : CTextInput(Pos)
  108. {
  109. originalAlignment = alignment;
  110. setRedrawParent(true);
  111. createLabel(giveFocusToInput);
  112. setFont(font);
  113. setAlignment(alignment);
  114. }
  115. CTextInput::CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath & bgName)
  116. : CTextInput(Pos)
  117. {
  118. OBJECT_CONSTRUCTION;
  119. if (!bgName.empty())
  120. background = std::make_shared<CPicture>(bgName, bgOffset.x, bgOffset.y);
  121. else
  122. setRedrawParent(true);
  123. createLabel(true);
  124. }
  125. CTextInput::CTextInput(const Rect & Pos, std::shared_ptr<IImage> srf)
  126. : CTextInput(Pos)
  127. {
  128. OBJECT_CONSTRUCTION;
  129. background = std::make_shared<CPicture>(srf, Pos);
  130. pos.w = background->pos.w;
  131. pos.h = background->pos.h;
  132. background->pos = pos;
  133. createLabel(true);
  134. }
  135. void CTextInput::setFont(EFonts font)
  136. {
  137. label->font = font;
  138. }
  139. void CTextInput::setColor(const ColorRGBA & color)
  140. {
  141. label->color = color;
  142. }
  143. void CTextInput::setAlignment(ETextAlignment alignment)
  144. {
  145. originalAlignment = alignment;
  146. label->alignment = alignment;
  147. }
  148. const std::string & CTextInput::getText() const
  149. {
  150. return currentText;
  151. }
  152. void CTextInput::setCallback(const TextEditedCallback & cb)
  153. {
  154. assert(!onTextEdited);
  155. onTextEdited = cb;
  156. }
  157. void CTextInput::setPopupCallback(const std::function<void()> & cb)
  158. {
  159. callbackPopup = cb;
  160. }
  161. void CTextInput::setFilterFilename()
  162. {
  163. assert(!onTextFiltering);
  164. onTextFiltering = std::bind(&CTextInput::filenameFilter, _1, _2);
  165. }
  166. void CTextInput::setFilterNumber(int minValue, int maxValue, int metricDigits)
  167. {
  168. onTextFiltering = std::bind(&CTextInput::numberFilter, _1, _2, minValue, maxValue, metricDigits);
  169. }
  170. std::string CTextInput::getVisibleText() const
  171. {
  172. return hasFocus() ? currentText + composedText + "_" : currentText;
  173. }
  174. void CTextInput::showPopupWindow(const Point & cursorPosition)
  175. {
  176. if(callbackPopup)
  177. callbackPopup();
  178. }
  179. void CTextInput::clickPressed(const Point & cursorPosition)
  180. {
  181. // attempt to give focus unconditionally, even if we already have it
  182. // this forces on-screen keyboard to show up again, even if player have closed it before
  183. giveFocus();
  184. }
  185. void CTextInput::keyPressed(EShortcut key)
  186. {
  187. if(!hasFocus())
  188. return;
  189. if(key == EShortcut::GLOBAL_MOVE_FOCUS)
  190. {
  191. moveFocus();
  192. return;
  193. }
  194. bool redrawNeeded = false;
  195. switch(key)
  196. {
  197. case EShortcut::GLOBAL_BACKSPACE:
  198. if(!composedText.empty())
  199. {
  200. TextOperations::trimRightUnicode(composedText);
  201. redrawNeeded = true;
  202. }
  203. else if(!currentText.empty())
  204. {
  205. TextOperations::trimRightUnicode(currentText);
  206. redrawNeeded = true;
  207. }
  208. break;
  209. default:
  210. break;
  211. }
  212. if(redrawNeeded)
  213. {
  214. std::string oldText = currentText;
  215. if(onTextFiltering)
  216. onTextFiltering(currentText, oldText);
  217. updateLabel();
  218. if(onTextEdited)
  219. onTextEdited(currentText);
  220. }
  221. }
  222. void CTextInput::setText(const std::string & nText)
  223. {
  224. currentText = nText;
  225. updateLabel();
  226. }
  227. void CTextInput::updateLabel()
  228. {
  229. std::string visibleText = getVisibleText();
  230. label->alignment = originalAlignment;
  231. const auto & font = ENGINE->renderHandler().loadFont(label->font);
  232. while ((font->getStringWidth(visibleText) - CLabel::getDelimitersWidth(label->font, visibleText)) > pos.w)
  233. {
  234. label->alignment = ETextAlignment::CENTERRIGHT;
  235. visibleText = visibleText.substr(TextOperations::getUnicodeCharacterSize(visibleText[0]));
  236. }
  237. label->setText(visibleText);
  238. }
  239. void CTextInput::textInputted(const std::string & enteredText)
  240. {
  241. if(!hasFocus())
  242. return;
  243. std::string oldText = currentText;
  244. setText(getText() + enteredText);
  245. if(onTextFiltering)
  246. onTextFiltering(currentText, oldText);
  247. updateLabel();
  248. if(currentText != oldText)
  249. {
  250. if(onTextEdited)
  251. onTextEdited(currentText);
  252. }
  253. composedText.clear();
  254. }
  255. void CTextInput::textEdited(const std::string & enteredText)
  256. {
  257. if(!hasFocus())
  258. return;
  259. composedText = enteredText;
  260. updateLabel();
  261. }
  262. void CTextInput::filenameFilter(std::string & text, const std::string &oldText)
  263. {
  264. static const std::string forbiddenChars = "<>:\"/\\|?*\r\n"; //if we are entering a filename, some special characters won't be allowed
  265. size_t pos;
  266. while((pos = text.find_first_of(forbiddenChars)) != std::string::npos)
  267. text.erase(pos, 1);
  268. }
  269. std::optional<char> getMetricSuffix(const std::string& text)
  270. {
  271. const std::string suffixes = "kKmMgGtTpPeE";
  272. std::vector<char> found;
  273. // Collect all suffixes in the string
  274. for (char c : text) {
  275. if (suffixes.find(c) != std::string::npos) {
  276. // Normalize: 'k' lowercase, others uppercase
  277. found.push_back((c == 'k' || c == 'K') ? 'k' : static_cast<char>(std::toupper(c)));
  278. }
  279. }
  280. if (found.empty()) return std::nullopt; // No suffix
  281. if (found.size() == 1) return found[0]; // Single suffix
  282. // More than one suffix
  283. bool allSame = std::all_of(found.begin(), found.end(), [&](char c){ return c == found[0]; });
  284. if (allSame) return std::nullopt; // Multiple but identical → nullopt
  285. return found.back(); // Multiple different → last suffix
  286. }
  287. void CTextInput::numberFilter(std::string & text, const std::string & oldText, int minValue, int maxValue, int metricDigits)
  288. {
  289. assert(minValue < maxValue);
  290. bool isNegative = std::count_if(text.begin(), text.end(), [](char c){ return c == '-'; }) == 1 && minValue < 0;
  291. auto suffix = getMetricSuffix(text);
  292. if(metricDigits == 0)
  293. suffix = std::nullopt;
  294. // Remove all non-digit characters
  295. text.erase(std::remove_if(text.begin(), text.end(), [](char c){ return !isdigit(c); }), text.end());
  296. // Remove leading zeros
  297. size_t firstNonZero = text.find_first_not_of('0');
  298. if (firstNonZero > 0)
  299. text.erase(0, firstNonZero);
  300. if (text.empty())
  301. text = "0";
  302. // Add negative sign
  303. text = (isNegative ? "-" : "") + text;
  304. // Restore suffix if it exists
  305. if (suffix)
  306. text += *suffix;
  307. // Clamp value
  308. int value = TextOperations::parseMetric<int>(text);
  309. if (metricDigits)
  310. text = (isNegative && value == 0 ? "-" : "") + TextOperations::formatMetric(value, metricDigits);
  311. if (value < minValue)
  312. text = metricDigits ? TextOperations::formatMetric(minValue, metricDigits) : std::to_string(minValue);
  313. else if (value > maxValue)
  314. text = metricDigits ? TextOperations::formatMetric(maxValue, metricDigits) : std::to_string(maxValue);
  315. }
  316. void CTextInput::activate()
  317. {
  318. CFocusable::activate();
  319. if (hasFocus())
  320. {
  321. #if defined(VCMI_MOBILE)
  322. //giveFocus();
  323. #else
  324. ENGINE->input().startTextInput(pos);
  325. #endif
  326. }
  327. }
  328. void CTextInput::deactivate()
  329. {
  330. CFocusable::deactivate();
  331. if (hasFocus())
  332. {
  333. #if defined(VCMI_MOBILE)
  334. removeFocus();
  335. #else
  336. ENGINE->input().stopTextInput();
  337. #endif
  338. }
  339. }
  340. void CTextInput::onFocusGot()
  341. {
  342. updateLabel();
  343. }
  344. void CTextInput::onFocusLost()
  345. {
  346. updateLabel();
  347. }
  348. void CFocusable::focusGot()
  349. {
  350. if (isActive())
  351. ENGINE->input().startTextInput(pos);
  352. onFocusGot();
  353. }
  354. void CFocusable::focusLost()
  355. {
  356. if (isActive())
  357. ENGINE->input().stopTextInput();
  358. onFocusLost();
  359. }
  360. CFocusable::CFocusable()
  361. {
  362. focusables.push_back(this);
  363. }
  364. CFocusable::~CFocusable()
  365. {
  366. if(hasFocus())
  367. inputWithFocus = nullptr;
  368. focusables -= this;
  369. }
  370. bool CFocusable::hasFocus() const
  371. {
  372. return inputWithFocus == this;
  373. }
  374. void CFocusable::giveFocus()
  375. {
  376. auto previousInput = inputWithFocus;
  377. inputWithFocus = this;
  378. if(previousInput)
  379. previousInput->focusLost();
  380. focusGot();
  381. }
  382. void CFocusable::moveFocus()
  383. {
  384. auto i = vstd::find(focusables, this);
  385. auto ourIt = i;
  386. for(i++; i != ourIt; i++)
  387. {
  388. if(i == focusables.end())
  389. i = focusables.begin();
  390. if(*i == this)
  391. return;
  392. if((*i)->isActive())
  393. {
  394. (*i)->giveFocus();
  395. break;
  396. }
  397. }
  398. }
  399. void CFocusable::removeFocus()
  400. {
  401. if(this == inputWithFocus)
  402. {
  403. inputWithFocus = nullptr;
  404. focusLost();
  405. }
  406. }