Console.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. // Copyright (c) 2008-2023 the Urho3D project
  2. // License: MIT
  3. #include "../Precompiled.h"
  4. #include "../Core/Context.h"
  5. #include "../Core/CoreEvents.h"
  6. #include "../Engine/Console.h"
  7. #include "../Engine/EngineEvents.h"
  8. #include "../Graphics/Graphics.h"
  9. #include "../Input/Input.h"
  10. #include "../IO/IOEvents.h"
  11. #include "../IO/Log.h"
  12. #include "../Resource/ResourceCache.h"
  13. #include "../UI/DropDownList.h"
  14. #include "../UI/Font.h"
  15. #include "../UI/LineEdit.h"
  16. #include "../UI/ListView.h"
  17. #include "../UI/ScrollBar.h"
  18. #include "../UI/Text.h"
  19. #include "../UI/UI.h"
  20. #include "../UI/UIEvents.h"
  21. #include <algorithm>
  22. #include "../DebugNew.h"
  23. namespace Urho3D
  24. {
  25. static const int DEFAULT_CONSOLE_ROWS = 16;
  26. static const int DEFAULT_HISTORY_SIZE = 16;
  27. const char* logStyles[] =
  28. {
  29. "ConsoleTraceText",
  30. "ConsoleDebugText",
  31. "ConsoleInfoText",
  32. "ConsoleWarningText",
  33. "ConsoleErrorText",
  34. "ConsoleText"
  35. };
  36. Console::Console(Context* context) :
  37. Object(context),
  38. autoVisibleOnError_(false),
  39. historyRows_(DEFAULT_HISTORY_SIZE),
  40. historyPosition_(0),
  41. autoCompletePosition_(0),
  42. historyOrAutoCompleteChange_(false),
  43. printing_(false)
  44. {
  45. auto* ui = GetSubsystem<UI>();
  46. UIElement* uiRoot = ui->GetRoot();
  47. // By default prevent the automatic showing of the screen keyboard
  48. focusOnShow_ = !ui->GetUseScreenKeyboard();
  49. background_ = uiRoot->CreateChild<BorderImage>();
  50. background_->SetBringToBack(false);
  51. background_->SetClipChildren(true);
  52. background_->SetEnabled(true);
  53. background_->SetVisible(false); // Hide by default
  54. background_->SetPriority(200); // Show on top of the debug HUD
  55. background_->SetBringToBack(false);
  56. background_->SetLayout(LM_VERTICAL);
  57. rowContainer_ = background_->CreateChild<ListView>();
  58. rowContainer_->SetHighlightMode(HM_ALWAYS);
  59. rowContainer_->SetMultiselect(true);
  60. commandLine_ = background_->CreateChild<UIElement>();
  61. commandLine_->SetLayoutMode(LM_HORIZONTAL);
  62. commandLine_->SetLayoutSpacing(1);
  63. interpreters_ = commandLine_->CreateChild<DropDownList>();
  64. lineEdit_ = commandLine_->CreateChild<LineEdit>();
  65. lineEdit_->SetFocusMode(FM_FOCUSABLE); // Do not allow defocus with ESC
  66. closeButton_ = uiRoot->CreateChild<Button>();
  67. closeButton_->SetVisible(false);
  68. closeButton_->SetPriority(background_->GetPriority() + 1); // Show on top of console's background
  69. closeButton_->SetBringToBack(false);
  70. SetNumRows(DEFAULT_CONSOLE_ROWS);
  71. SubscribeToEvent(interpreters_, E_ITEMSELECTED, URHO3D_HANDLER(Console, HandleInterpreterSelected));
  72. SubscribeToEvent(lineEdit_, E_TEXTCHANGED, URHO3D_HANDLER(Console, HandleTextChanged));
  73. SubscribeToEvent(lineEdit_, E_TEXTFINISHED, URHO3D_HANDLER(Console, HandleTextFinished));
  74. SubscribeToEvent(lineEdit_, E_UNHANDLEDKEY, URHO3D_HANDLER(Console, HandleLineEditKey));
  75. SubscribeToEvent(closeButton_, E_RELEASED, URHO3D_HANDLER(Console, HandleCloseButtonPressed));
  76. SubscribeToEvent(uiRoot, E_RESIZED, URHO3D_HANDLER(Console, HandleRootElementResized));
  77. SubscribeToEvent(E_LOGMESSAGE, URHO3D_HANDLER(Console, HandleLogMessage));
  78. SubscribeToEvent(E_POSTUPDATE, URHO3D_HANDLER(Console, HandlePostUpdate));
  79. }
  80. Console::~Console()
  81. {
  82. background_->Remove();
  83. closeButton_->Remove();
  84. }
  85. void Console::SetDefaultStyle(XMLFile* style)
  86. {
  87. if (!style)
  88. return;
  89. background_->SetDefaultStyle(style);
  90. background_->SetStyle("ConsoleBackground");
  91. rowContainer_->SetStyleAuto();
  92. for (i32 i = 0; i < rowContainer_->GetNumItems(); ++i)
  93. rowContainer_->GetItem(i)->SetStyle("ConsoleText");
  94. interpreters_->SetStyleAuto();
  95. for (i32 i = 0; i < interpreters_->GetNumItems(); ++i)
  96. interpreters_->GetItem(i)->SetStyle("ConsoleText");
  97. lineEdit_->SetStyle("ConsoleLineEdit");
  98. closeButton_->SetDefaultStyle(style);
  99. closeButton_->SetStyle("CloseButton");
  100. UpdateElements();
  101. }
  102. void Console::SetVisible(bool enable)
  103. {
  104. auto* input = GetSubsystem<Input>();
  105. auto* ui = GetSubsystem<UI>();
  106. Cursor* cursor = ui->GetCursor();
  107. background_->SetVisible(enable);
  108. closeButton_->SetVisible(enable);
  109. if (enable)
  110. {
  111. // Check if we have receivers for E_CONSOLECOMMAND every time here in case the handler is being added later dynamically
  112. bool hasInterpreter = PopulateInterpreter();
  113. commandLine_->SetVisible(hasInterpreter);
  114. if (hasInterpreter && focusOnShow_)
  115. ui->SetFocusElement(lineEdit_);
  116. // Ensure the background has no empty space when shown without the lineedit
  117. background_->SetHeight(background_->GetMinHeight());
  118. if (!cursor)
  119. {
  120. // Show OS mouse
  121. input->SetMouseMode(MM_FREE, true);
  122. input->SetMouseVisible(true, true);
  123. }
  124. input->SetMouseGrabbed(false, true);
  125. }
  126. else
  127. {
  128. rowContainer_->SetFocus(false);
  129. interpreters_->SetFocus(false);
  130. lineEdit_->SetFocus(false);
  131. if (!cursor)
  132. {
  133. // Restore OS mouse visibility
  134. input->ResetMouseMode();
  135. input->ResetMouseVisible();
  136. }
  137. input->ResetMouseGrabbed();
  138. }
  139. }
  140. void Console::Toggle()
  141. {
  142. SetVisible(!IsVisible());
  143. }
  144. void Console::SetNumBufferedRows(i32 rows)
  145. {
  146. assert(rows >= 0);
  147. if (rows < displayedRows_)
  148. return;
  149. rowContainer_->DisableLayoutUpdate();
  150. int delta = rowContainer_->GetNumItems() - rows;
  151. if (delta > 0)
  152. {
  153. // We have more, remove oldest rows first
  154. for (int i = 0; i < delta; ++i)
  155. rowContainer_->RemoveItem(0);
  156. }
  157. else
  158. {
  159. // We have less, add more rows at the top
  160. for (int i = 0; i > delta; --i)
  161. {
  162. auto* text = new Text(context_);
  163. // If style is already set, apply here to ensure proper height of the console when
  164. // amount of rows is changed
  165. if (background_->GetDefaultStyle())
  166. text->SetStyle("ConsoleText");
  167. rowContainer_->InsertItem(0, text);
  168. }
  169. }
  170. rowContainer_->EnsureItemVisibility(rowContainer_->GetItem(rowContainer_->GetNumItems() - 1));
  171. rowContainer_->EnableLayoutUpdate();
  172. rowContainer_->UpdateLayout();
  173. UpdateElements();
  174. }
  175. void Console::SetNumRows(i32 rows)
  176. {
  177. assert(rows >= 0);
  178. if (!rows)
  179. return;
  180. displayedRows_ = rows;
  181. if (GetNumBufferedRows() < rows)
  182. SetNumBufferedRows(rows);
  183. UpdateElements();
  184. }
  185. void Console::SetNumHistoryRows(i32 rows)
  186. {
  187. assert(rows >= 0);
  188. historyRows_ = rows;
  189. if (history_.Size() > rows)
  190. history_.Resize(rows);
  191. if (historyPosition_ > rows)
  192. historyPosition_ = rows;
  193. }
  194. void Console::SetFocusOnShow(bool enable)
  195. {
  196. focusOnShow_ = enable;
  197. }
  198. void Console::AddAutoComplete(const String& option)
  199. {
  200. // Sorted insertion
  201. Vector<String>::Iterator iter = std::upper_bound(autoComplete_.Begin(), autoComplete_.End(), option);
  202. if (!iter.ptr_)
  203. autoComplete_.Push(option);
  204. // Make sure it isn't a duplicate
  205. else if (iter == autoComplete_.Begin() || *(iter - 1) != option)
  206. autoComplete_.Insert(iter, option);
  207. }
  208. void Console::RemoveAutoComplete(const String& option)
  209. {
  210. // Erase and keep ordered
  211. autoComplete_.Erase(std::lower_bound(autoComplete_.Begin(), autoComplete_.End(), option));
  212. if (autoCompletePosition_ > autoComplete_.Size())
  213. autoCompletePosition_ = autoComplete_.Size();
  214. }
  215. void Console::UpdateElements()
  216. {
  217. int width = GetSubsystem<UI>()->GetRoot()->GetWidth();
  218. const IntRect& border = background_->GetLayoutBorder();
  219. const IntRect& panelBorder = rowContainer_->GetScrollPanel()->GetClipBorder();
  220. rowContainer_->SetFixedWidth(width - border.left_ - border.right_);
  221. rowContainer_->SetFixedHeight(
  222. displayedRows_ * rowContainer_->GetItem((unsigned)0)->GetHeight() + panelBorder.top_ + panelBorder.bottom_ +
  223. (rowContainer_->GetHorizontalScrollBar()->IsVisible() ? rowContainer_->GetHorizontalScrollBar()->GetHeight() : 0));
  224. background_->SetFixedWidth(width);
  225. background_->SetHeight(background_->GetMinHeight());
  226. }
  227. XMLFile* Console::GetDefaultStyle() const
  228. {
  229. return background_->GetDefaultStyle(false);
  230. }
  231. bool Console::IsVisible() const
  232. {
  233. return background_ && background_->IsVisible();
  234. }
  235. i32 Console::GetNumBufferedRows() const
  236. {
  237. return rowContainer_->GetNumItems();
  238. }
  239. void Console::CopySelectedRows() const
  240. {
  241. rowContainer_->CopySelectedItemsToClipboard();
  242. }
  243. const String& Console::GetHistoryRow(i32 index) const
  244. {
  245. assert(index >= 0);
  246. return index < history_.Size() ? history_[index] : String::EMPTY;
  247. }
  248. bool Console::PopulateInterpreter()
  249. {
  250. interpreters_->RemoveAllItems();
  251. EventReceiverGroup* group = context_->GetEventReceivers(E_CONSOLECOMMAND);
  252. if (!group || group->receivers_.Empty())
  253. return false;
  254. Vector<String> names;
  255. for (const Object* receiver : group->receivers_)
  256. {
  257. if (receiver)
  258. names.Push(receiver->GetTypeName());
  259. }
  260. Sort(names.Begin(), names.End());
  261. i32 selection = NINDEX;
  262. for (i32 i = 0; i < names.Size(); ++i)
  263. {
  264. const String& name = names[i];
  265. if (name == commandInterpreter_)
  266. selection = i;
  267. Text* text = new Text(context_);
  268. text->SetStyle("ConsoleText");
  269. text->SetText(name);
  270. interpreters_->AddItem(text);
  271. }
  272. const IntRect& border = interpreters_->GetPopup()->GetLayoutBorder();
  273. interpreters_->SetMaxWidth(interpreters_->GetListView()->GetContentElement()->GetWidth() + border.left_ + border.right_);
  274. bool enabled = interpreters_->GetNumItems() > 1;
  275. interpreters_->SetEnabled(enabled);
  276. interpreters_->SetFocusMode(enabled ? FM_FOCUSABLE_DEFOCUSABLE : FM_NOTFOCUSABLE);
  277. if (selection == NINDEX)
  278. {
  279. selection = 0;
  280. commandInterpreter_ = names[selection];
  281. }
  282. interpreters_->SetSelection(selection);
  283. return true;
  284. }
  285. void Console::HandleInterpreterSelected(StringHash eventType, VariantMap& eventData)
  286. {
  287. commandInterpreter_ = static_cast<Text*>(interpreters_->GetSelectedItem())->GetText();
  288. lineEdit_->SetFocus(true);
  289. }
  290. void Console::HandleTextChanged(StringHash eventType, VariantMap & eventData)
  291. {
  292. // Save the original line
  293. // Make sure the change isn't caused by auto complete or history
  294. if (!historyOrAutoCompleteChange_)
  295. autoCompleteLine_ = eventData[TextEntry::P_TEXT].GetString();
  296. historyOrAutoCompleteChange_ = false;
  297. }
  298. void Console::HandleTextFinished(StringHash eventType, VariantMap& eventData)
  299. {
  300. using namespace TextFinished;
  301. String line = lineEdit_->GetText();
  302. if (!line.Empty())
  303. {
  304. // Send the command as an event for script subsystem
  305. using namespace ConsoleCommand;
  306. SendEvent(E_CONSOLECOMMAND,
  307. P_COMMAND, line,
  308. P_ID, static_cast<Text*>(interpreters_->GetSelectedItem())->GetText());
  309. // Make sure the line isn't the same as the last one
  310. if (history_.Empty() || line != history_.Back())
  311. {
  312. // Store to history, then clear the lineedit
  313. history_.Push(line);
  314. if (history_.Size() > historyRows_)
  315. history_.Erase(history_.Begin());
  316. }
  317. historyPosition_ = history_.Size(); // Reset
  318. autoCompletePosition_ = autoComplete_.Size(); // Reset
  319. currentRow_.Clear();
  320. lineEdit_->SetText(currentRow_);
  321. }
  322. }
  323. void Console::HandleLineEditKey(StringHash eventType, VariantMap& eventData)
  324. {
  325. if (!historyRows_)
  326. return;
  327. using namespace UnhandledKey;
  328. bool changed = false;
  329. switch (eventData[P_KEY].GetI32())
  330. {
  331. case KEY_UP:
  332. if (autoCompletePosition_ == 0)
  333. autoCompletePosition_ = autoComplete_.Size();
  334. if (autoCompletePosition_ < autoComplete_.Size())
  335. {
  336. // Search for auto completion that contains the contents of the line
  337. for (--autoCompletePosition_; autoCompletePosition_ >= 0; --autoCompletePosition_)
  338. {
  339. const String& current = autoComplete_[autoCompletePosition_];
  340. if (current.StartsWith(autoCompleteLine_))
  341. {
  342. historyOrAutoCompleteChange_ = true;
  343. lineEdit_->SetText(current);
  344. break;
  345. }
  346. }
  347. // If not found
  348. if (autoCompletePosition_ < 0)
  349. {
  350. // Reset the position
  351. autoCompletePosition_ = autoComplete_.Size();
  352. // Reset history position
  353. historyPosition_ = history_.Size();
  354. }
  355. }
  356. // If no more auto complete options and history options left
  357. if (autoCompletePosition_ == autoComplete_.Size() && historyPosition_ > 0)
  358. {
  359. // If line text is not a history, save the current text value to be restored later
  360. if (historyPosition_ == history_.Size())
  361. currentRow_ = lineEdit_->GetText();
  362. // Use the previous option
  363. --historyPosition_;
  364. changed = true;
  365. }
  366. break;
  367. case KEY_DOWN:
  368. // If history options left
  369. if (historyPosition_ < history_.Size())
  370. {
  371. // Use the next option
  372. ++historyPosition_;
  373. changed = true;
  374. }
  375. else
  376. {
  377. // Loop over
  378. if (autoCompletePosition_ >= autoComplete_.Size())
  379. autoCompletePosition_ = 0;
  380. else
  381. ++autoCompletePosition_; // If not starting over, skip checking the currently found completion
  382. i32 startPosition = autoCompletePosition_;
  383. // Search for auto completion that contains the contents of the line
  384. for (; autoCompletePosition_ < autoComplete_.Size(); ++autoCompletePosition_)
  385. {
  386. const String& current = autoComplete_[autoCompletePosition_];
  387. if (current.StartsWith(autoCompleteLine_))
  388. {
  389. historyOrAutoCompleteChange_ = true;
  390. lineEdit_->SetText(current);
  391. break;
  392. }
  393. }
  394. // Continue to search the complete range
  395. if (autoCompletePosition_ == autoComplete_.Size())
  396. {
  397. for (autoCompletePosition_ = 0; autoCompletePosition_ != startPosition; ++autoCompletePosition_)
  398. {
  399. const String& current = autoComplete_[autoCompletePosition_];
  400. if (current.StartsWith(autoCompleteLine_))
  401. {
  402. historyOrAutoCompleteChange_ = true;
  403. lineEdit_->SetText(current);
  404. break;
  405. }
  406. }
  407. }
  408. }
  409. break;
  410. default: break;
  411. }
  412. if (changed)
  413. {
  414. historyOrAutoCompleteChange_ = true;
  415. // Set text to history option
  416. if (historyPosition_ < history_.Size())
  417. lineEdit_->SetText(history_[historyPosition_]);
  418. else // restore the original line value before it was set to history values
  419. {
  420. lineEdit_->SetText(currentRow_);
  421. // Set the auto complete position according to the currentRow
  422. for (autoCompletePosition_ = 0; autoCompletePosition_ < autoComplete_.Size(); ++autoCompletePosition_)
  423. if (autoComplete_[autoCompletePosition_].StartsWith(currentRow_))
  424. break;
  425. }
  426. }
  427. }
  428. void Console::HandleCloseButtonPressed(StringHash eventType, VariantMap& eventData)
  429. {
  430. SetVisible(false);
  431. }
  432. void Console::HandleRootElementResized(StringHash eventType, VariantMap& eventData)
  433. {
  434. UpdateElements();
  435. }
  436. void Console::HandleLogMessage(StringHash eventType, VariantMap& eventData)
  437. {
  438. // If printing a log message causes more messages to be logged (error accessing font), disregard them
  439. if (printing_)
  440. return;
  441. using namespace LogMessage;
  442. int level = eventData[P_LEVEL].GetI32();
  443. // The message may be multi-line, so split to rows in that case
  444. Vector<String> rows = eventData[P_MESSAGE].GetString().Split('\n');
  445. for (const String& row : rows)
  446. pendingRows_.Push(MakePair(level, row));
  447. if (autoVisibleOnError_ && level == LOG_ERROR && !IsVisible())
  448. SetVisible(true);
  449. }
  450. void Console::HandlePostUpdate(StringHash eventType, VariantMap& eventData)
  451. {
  452. // Ensure UI-elements are not detached
  453. if (!background_->GetParent())
  454. {
  455. auto* ui = GetSubsystem<UI>();
  456. UIElement* uiRoot = ui->GetRoot();
  457. uiRoot->AddChild(background_);
  458. uiRoot->AddChild(closeButton_);
  459. }
  460. if (!rowContainer_->GetNumItems() || pendingRows_.Empty())
  461. return;
  462. printing_ = true;
  463. rowContainer_->DisableLayoutUpdate();
  464. Text* text = nullptr;
  465. for (const Pair<i32, String>& pendingRow : pendingRows_)
  466. {
  467. rowContainer_->RemoveItem(0);
  468. text = new Text(context_);
  469. text->SetText(pendingRow.second_);
  470. // Highlight console messages based on their type
  471. text->SetStyle(logStyles[pendingRow.first_]);
  472. rowContainer_->AddItem(text);
  473. }
  474. pendingRows_.Clear();
  475. rowContainer_->EnsureItemVisibility(text);
  476. rowContainer_->EnableLayoutUpdate();
  477. rowContainer_->UpdateLayout();
  478. UpdateElements(); // May need to readjust the height due to scrollbar visibility changes
  479. printing_ = false;
  480. }
  481. }