1 //
2 // Copyright (c) 2008-2017 the Urho3D project.
3 //
4 // Permission is hereby granted, free of charge, to any person obtaining a copy
5 // of this software and associated documentation files (the "Software"), to deal
6 // in the Software without restriction, including without limitation the rights
7 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 // copies of the Software, and to permit persons to whom the Software is
9 // furnished to do so, subject to the following conditions:
10 //
11 // The above copyright notice and this permission notice shall be included in
12 // all copies or substantial portions of the Software.
13 //
14 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 // THE SOFTWARE.
21 //
22
23 #include "../Precompiled.h"
24
25 #include "../Core/Context.h"
26 #include "../Core/CoreEvents.h"
27 #include "../Engine/Console.h"
28 #include "../Engine/EngineEvents.h"
29 #include "../Graphics/Graphics.h"
30 #include "../Input/Input.h"
31 #include "../IO/IOEvents.h"
32 #include "../IO/Log.h"
33 #include "../Resource/ResourceCache.h"
34 #include "../UI/DropDownList.h"
35 #include "../UI/Font.h"
36 #include "../UI/LineEdit.h"
37 #include "../UI/ListView.h"
38 #include "../UI/ScrollBar.h"
39 #include "../UI/Text.h"
40 #include "../UI/UI.h"
41 #include "../UI/UIEvents.h"
42
43 #include "../DebugNew.h"
44
45 namespace Urho3D
46 {
47
48 static const int DEFAULT_CONSOLE_ROWS = 16;
49 static const int DEFAULT_HISTORY_SIZE = 16;
50
51 const char* logStyles[] =
52 {
53 "ConsoleDebugText",
54 "ConsoleInfoText",
55 "ConsoleWarningText",
56 "ConsoleErrorText",
57 "ConsoleText"
58 };
59
Console(Context * context)60 Console::Console(Context* context) :
61 Object(context),
62 autoVisibleOnError_(false),
63 historyRows_(DEFAULT_HISTORY_SIZE),
64 historyPosition_(0),
65 autoCompletePosition_(0),
66 historyOrAutoCompleteChange_(false),
67 printing_(false)
68 {
69 UI* ui = GetSubsystem<UI>();
70 UIElement* uiRoot = ui->GetRoot();
71
72 // By default prevent the automatic showing of the screen keyboard
73 focusOnShow_ = !ui->GetUseScreenKeyboard();
74
75 background_ = uiRoot->CreateChild<BorderImage>();
76 background_->SetBringToBack(false);
77 background_->SetClipChildren(true);
78 background_->SetEnabled(true);
79 background_->SetVisible(false); // Hide by default
80 background_->SetPriority(200); // Show on top of the debug HUD
81 background_->SetBringToBack(false);
82 background_->SetLayout(LM_VERTICAL);
83
84 rowContainer_ = background_->CreateChild<ListView>();
85 rowContainer_->SetHighlightMode(HM_ALWAYS);
86 rowContainer_->SetMultiselect(true);
87
88 commandLine_ = background_->CreateChild<UIElement>();
89 commandLine_->SetLayoutMode(LM_HORIZONTAL);
90 commandLine_->SetLayoutSpacing(1);
91 interpreters_ = commandLine_->CreateChild<DropDownList>();
92 lineEdit_ = commandLine_->CreateChild<LineEdit>();
93 lineEdit_->SetFocusMode(FM_FOCUSABLE); // Do not allow defocus with ESC
94
95 closeButton_ = uiRoot->CreateChild<Button>();
96 closeButton_->SetVisible(false);
97 closeButton_->SetPriority(background_->GetPriority() + 1); // Show on top of console's background
98 closeButton_->SetBringToBack(false);
99
100 SetNumRows(DEFAULT_CONSOLE_ROWS);
101
102 SubscribeToEvent(interpreters_, E_ITEMSELECTED, URHO3D_HANDLER(Console, HandleInterpreterSelected));
103 SubscribeToEvent(lineEdit_, E_TEXTCHANGED, URHO3D_HANDLER(Console, HandleTextChanged));
104 SubscribeToEvent(lineEdit_, E_TEXTFINISHED, URHO3D_HANDLER(Console, HandleTextFinished));
105 SubscribeToEvent(lineEdit_, E_UNHANDLEDKEY, URHO3D_HANDLER(Console, HandleLineEditKey));
106 SubscribeToEvent(closeButton_, E_RELEASED, URHO3D_HANDLER(Console, HandleCloseButtonPressed));
107 SubscribeToEvent(uiRoot, E_RESIZED, URHO3D_HANDLER(Console, HandleRootElementResized));
108 SubscribeToEvent(E_LOGMESSAGE, URHO3D_HANDLER(Console, HandleLogMessage));
109 SubscribeToEvent(E_POSTUPDATE, URHO3D_HANDLER(Console, HandlePostUpdate));
110 }
111
~Console()112 Console::~Console()
113 {
114 background_->Remove();
115 closeButton_->Remove();
116 }
117
SetDefaultStyle(XMLFile * style)118 void Console::SetDefaultStyle(XMLFile* style)
119 {
120 if (!style)
121 return;
122
123 background_->SetDefaultStyle(style);
124 background_->SetStyle("ConsoleBackground");
125 rowContainer_->SetStyleAuto();
126 for (unsigned i = 0; i < rowContainer_->GetNumItems(); ++i)
127 rowContainer_->GetItem(i)->SetStyle("ConsoleText");
128 interpreters_->SetStyleAuto();
129 for (unsigned i = 0; i < interpreters_->GetNumItems(); ++i)
130 interpreters_->GetItem(i)->SetStyle("ConsoleText");
131 lineEdit_->SetStyle("ConsoleLineEdit");
132
133 closeButton_->SetDefaultStyle(style);
134 closeButton_->SetStyle("CloseButton");
135
136 UpdateElements();
137 }
138
SetVisible(bool enable)139 void Console::SetVisible(bool enable)
140 {
141 Input* input = GetSubsystem<Input>();
142 UI* ui = GetSubsystem<UI>();
143 Cursor* cursor = ui->GetCursor();
144
145 background_->SetVisible(enable);
146 closeButton_->SetVisible(enable);
147
148 if (enable)
149 {
150 // Check if we have receivers for E_CONSOLECOMMAND every time here in case the handler is being added later dynamically
151 bool hasInterpreter = PopulateInterpreter();
152 commandLine_->SetVisible(hasInterpreter);
153 if (hasInterpreter && focusOnShow_)
154 ui->SetFocusElement(lineEdit_);
155
156 // Ensure the background has no empty space when shown without the lineedit
157 background_->SetHeight(background_->GetMinHeight());
158
159 if (!cursor)
160 {
161 // Show OS mouse
162 input->SetMouseMode(MM_FREE, true);
163 input->SetMouseVisible(true, true);
164 }
165
166 input->SetMouseGrabbed(false, true);
167 }
168 else
169 {
170 rowContainer_->SetFocus(false);
171 interpreters_->SetFocus(false);
172 lineEdit_->SetFocus(false);
173
174 if (!cursor)
175 {
176 // Restore OS mouse visibility
177 input->ResetMouseMode();
178 input->ResetMouseVisible();
179 }
180
181 input->ResetMouseGrabbed();
182 }
183 }
184
Toggle()185 void Console::Toggle()
186 {
187 SetVisible(!IsVisible());
188 }
189
SetNumBufferedRows(unsigned rows)190 void Console::SetNumBufferedRows(unsigned rows)
191 {
192 if (rows < displayedRows_)
193 return;
194
195 rowContainer_->DisableLayoutUpdate();
196
197 int delta = rowContainer_->GetNumItems() - rows;
198 if (delta > 0)
199 {
200 // We have more, remove oldest rows first
201 for (int i = 0; i < delta; ++i)
202 rowContainer_->RemoveItem((unsigned)0);
203 }
204 else
205 {
206 // We have less, add more rows at the top
207 for (int i = 0; i > delta; --i)
208 {
209 Text* text = new Text(context_);
210 // If style is already set, apply here to ensure proper height of the console when
211 // amount of rows is changed
212 if (background_->GetDefaultStyle())
213 text->SetStyle("ConsoleText");
214 rowContainer_->InsertItem(0, text);
215 }
216 }
217
218 rowContainer_->EnsureItemVisibility(rowContainer_->GetItem(rowContainer_->GetNumItems() - 1));
219 rowContainer_->EnableLayoutUpdate();
220 rowContainer_->UpdateLayout();
221
222 UpdateElements();
223 }
224
SetNumRows(unsigned rows)225 void Console::SetNumRows(unsigned rows)
226 {
227 if (!rows)
228 return;
229
230 displayedRows_ = rows;
231 if (GetNumBufferedRows() < rows)
232 SetNumBufferedRows(rows);
233
234 UpdateElements();
235 }
236
SetNumHistoryRows(unsigned rows)237 void Console::SetNumHistoryRows(unsigned rows)
238 {
239 historyRows_ = rows;
240 if (history_.Size() > rows)
241 history_.Resize(rows);
242 if (historyPosition_ > rows)
243 historyPosition_ = rows;
244 }
245
SetFocusOnShow(bool enable)246 void Console::SetFocusOnShow(bool enable)
247 {
248 focusOnShow_ = enable;
249 }
250
AddAutoComplete(const String & option)251 void Console::AddAutoComplete(const String& option)
252 {
253 // Sorted insertion
254 Vector<String>::Iterator iter = UpperBound(autoComplete_.Begin(), autoComplete_.End(), option);
255 if (!iter.ptr_)
256 autoComplete_.Push(option);
257 // Make sure it isn't a duplicate
258 else if (iter == autoComplete_.Begin() || *(iter - 1) != option)
259 autoComplete_.Insert(iter, option);
260 }
261
RemoveAutoComplete(const String & option)262 void Console::RemoveAutoComplete(const String& option)
263 {
264 // Erase and keep ordered
265 autoComplete_.Erase(LowerBound(autoComplete_.Begin(), autoComplete_.End(), option));
266 if (autoCompletePosition_ > autoComplete_.Size())
267 autoCompletePosition_ = autoComplete_.Size();
268 }
269
UpdateElements()270 void Console::UpdateElements()
271 {
272 int width = GetSubsystem<UI>()->GetRoot()->GetWidth();
273 const IntRect& border = background_->GetLayoutBorder();
274 const IntRect& panelBorder = rowContainer_->GetScrollPanel()->GetClipBorder();
275 rowContainer_->SetFixedWidth(width - border.left_ - border.right_);
276 rowContainer_->SetFixedHeight(
277 displayedRows_ * rowContainer_->GetItem((unsigned)0)->GetHeight() + panelBorder.top_ + panelBorder.bottom_ +
278 (rowContainer_->GetHorizontalScrollBar()->IsVisible() ? rowContainer_->GetHorizontalScrollBar()->GetHeight() : 0));
279 background_->SetFixedWidth(width);
280 background_->SetHeight(background_->GetMinHeight());
281 }
282
GetDefaultStyle() const283 XMLFile* Console::GetDefaultStyle() const
284 {
285 return background_->GetDefaultStyle(false);
286 }
287
IsVisible() const288 bool Console::IsVisible() const
289 {
290 return background_ && background_->IsVisible();
291 }
292
GetNumBufferedRows() const293 unsigned Console::GetNumBufferedRows() const
294 {
295 return rowContainer_->GetNumItems();
296 }
297
CopySelectedRows() const298 void Console::CopySelectedRows() const
299 {
300 rowContainer_->CopySelectedItemsToClipboard();
301 }
302
GetHistoryRow(unsigned index) const303 const String& Console::GetHistoryRow(unsigned index) const
304 {
305 return index < history_.Size() ? history_[index] : String::EMPTY;
306 }
307
PopulateInterpreter()308 bool Console::PopulateInterpreter()
309 {
310 interpreters_->RemoveAllItems();
311
312 EventReceiverGroup* group = context_->GetEventReceivers(E_CONSOLECOMMAND);
313 if (!group || group->receivers_.Empty())
314 return false;
315
316 Vector<String> names;
317 for (unsigned i = 0; i < group->receivers_.Size(); ++i)
318 {
319 Object* receiver = group->receivers_[i];
320 if (receiver)
321 names.Push(receiver->GetTypeName());
322 }
323 Sort(names.Begin(), names.End());
324
325 unsigned selection = M_MAX_UNSIGNED;
326 for (unsigned i = 0; i < names.Size(); ++i)
327 {
328 const String& name = names[i];
329 if (name == commandInterpreter_)
330 selection = i;
331 Text* text = new Text(context_);
332 text->SetStyle("ConsoleText");
333 text->SetText(name);
334 interpreters_->AddItem(text);
335 }
336
337 const IntRect& border = interpreters_->GetPopup()->GetLayoutBorder();
338 interpreters_->SetMaxWidth(interpreters_->GetListView()->GetContentElement()->GetWidth() + border.left_ + border.right_);
339 bool enabled = interpreters_->GetNumItems() > 1;
340 interpreters_->SetEnabled(enabled);
341 interpreters_->SetFocusMode(enabled ? FM_FOCUSABLE_DEFOCUSABLE : FM_NOTFOCUSABLE);
342
343 if (selection == M_MAX_UNSIGNED)
344 {
345 selection = 0;
346 commandInterpreter_ = names[selection];
347 }
348 interpreters_->SetSelection(selection);
349
350 return true;
351 }
352
HandleInterpreterSelected(StringHash eventType,VariantMap & eventData)353 void Console::HandleInterpreterSelected(StringHash eventType, VariantMap& eventData)
354 {
355 commandInterpreter_ = static_cast<Text*>(interpreters_->GetSelectedItem())->GetText();
356 lineEdit_->SetFocus(true);
357 }
358
HandleTextChanged(StringHash eventType,VariantMap & eventData)359 void Console::HandleTextChanged(StringHash eventType, VariantMap & eventData)
360 {
361 // Save the original line
362 // Make sure the change isn't caused by auto complete or history
363 if (!historyOrAutoCompleteChange_)
364 autoCompleteLine_ = eventData[TextEntry::P_TEXT].GetString();
365
366 historyOrAutoCompleteChange_ = false;
367 }
368
HandleTextFinished(StringHash eventType,VariantMap & eventData)369 void Console::HandleTextFinished(StringHash eventType, VariantMap& eventData)
370 {
371 using namespace TextFinished;
372
373 String line = lineEdit_->GetText();
374 if (!line.Empty())
375 {
376 // Send the command as an event for script subsystem
377 using namespace ConsoleCommand;
378
379 #if URHO3D_CXX11
380 SendEvent(E_CONSOLECOMMAND, P_COMMAND, line, P_ID, static_cast<Text*>(interpreters_->GetSelectedItem())->GetText());
381 #else
382 VariantMap& newEventData = GetEventDataMap();
383 newEventData[P_COMMAND] = line;
384 newEventData[P_ID] = static_cast<Text*>(interpreters_->GetSelectedItem())->GetText();
385 SendEvent(E_CONSOLECOMMAND, newEventData);
386 #endif
387
388 // Make sure the line isn't the same as the last one
389 if (history_.Empty() || line != history_.Back())
390 {
391 // Store to history, then clear the lineedit
392 history_.Push(line);
393 if (history_.Size() > historyRows_)
394 history_.Erase(history_.Begin());
395 }
396
397 historyPosition_ = history_.Size(); // Reset
398 autoCompletePosition_ = autoComplete_.Size(); // Reset
399
400 currentRow_.Clear();
401 lineEdit_->SetText(currentRow_);
402 }
403 }
404
HandleLineEditKey(StringHash eventType,VariantMap & eventData)405 void Console::HandleLineEditKey(StringHash eventType, VariantMap& eventData)
406 {
407 if (!historyRows_)
408 return;
409
410 using namespace UnhandledKey;
411
412 bool changed = false;
413
414 switch (eventData[P_KEY].GetInt())
415 {
416 case KEY_UP:
417 if (autoCompletePosition_ == 0)
418 autoCompletePosition_ = autoComplete_.Size();
419
420 if (autoCompletePosition_ < autoComplete_.Size())
421 {
422 // Search for auto completion that contains the contents of the line
423 for (--autoCompletePosition_; autoCompletePosition_ != M_MAX_UNSIGNED; --autoCompletePosition_)
424 {
425 const String& current = autoComplete_[autoCompletePosition_];
426 if (current.StartsWith(autoCompleteLine_))
427 {
428 historyOrAutoCompleteChange_ = true;
429 lineEdit_->SetText(current);
430 break;
431 }
432 }
433
434 // If not found
435 if (autoCompletePosition_ == M_MAX_UNSIGNED)
436 {
437 // Reset the position
438 autoCompletePosition_ = autoComplete_.Size();
439 // Reset history position
440 historyPosition_ = history_.Size();
441 }
442 }
443
444 // If no more auto complete options and history options left
445 if (autoCompletePosition_ == autoComplete_.Size() && historyPosition_ > 0)
446 {
447 // If line text is not a history, save the current text value to be restored later
448 if (historyPosition_ == history_.Size())
449 currentRow_ = lineEdit_->GetText();
450 // Use the previous option
451 --historyPosition_;
452 changed = true;
453 }
454 break;
455
456 case KEY_DOWN:
457 // If history options left
458 if (historyPosition_ < history_.Size())
459 {
460 // Use the next option
461 ++historyPosition_;
462 changed = true;
463 }
464 else
465 {
466 // Loop over
467 if (autoCompletePosition_ >= autoComplete_.Size())
468 autoCompletePosition_ = 0;
469 else
470 ++autoCompletePosition_; // If not starting over, skip checking the currently found completion
471
472 unsigned startPosition = autoCompletePosition_;
473
474 // Search for auto completion that contains the contents of the line
475 for (; autoCompletePosition_ < autoComplete_.Size(); ++autoCompletePosition_)
476 {
477 const String& current = autoComplete_[autoCompletePosition_];
478 if (current.StartsWith(autoCompleteLine_))
479 {
480 historyOrAutoCompleteChange_ = true;
481 lineEdit_->SetText(current);
482 break;
483 }
484 }
485
486 // Continue to search the complete range
487 if (autoCompletePosition_ == autoComplete_.Size())
488 {
489 for (autoCompletePosition_ = 0; autoCompletePosition_ != startPosition; ++autoCompletePosition_)
490 {
491 const String& current = autoComplete_[autoCompletePosition_];
492 if (current.StartsWith(autoCompleteLine_))
493 {
494 historyOrAutoCompleteChange_ = true;
495 lineEdit_->SetText(current);
496 break;
497 }
498 }
499 }
500 }
501 break;
502
503 default: break;
504 }
505
506 if (changed)
507 {
508 historyOrAutoCompleteChange_ = true;
509 // Set text to history option
510 if (historyPosition_ < history_.Size())
511 lineEdit_->SetText(history_[historyPosition_]);
512 else // restore the original line value before it was set to history values
513 {
514 lineEdit_->SetText(currentRow_);
515 // Set the auto complete position according to the currentRow
516 for (autoCompletePosition_ = 0; autoCompletePosition_ < autoComplete_.Size(); ++autoCompletePosition_)
517 if (autoComplete_[autoCompletePosition_].StartsWith(currentRow_))
518 break;
519 }
520 }
521 }
522
HandleCloseButtonPressed(StringHash eventType,VariantMap & eventData)523 void Console::HandleCloseButtonPressed(StringHash eventType, VariantMap& eventData)
524 {
525 SetVisible(false);
526 }
527
HandleRootElementResized(StringHash eventType,VariantMap & eventData)528 void Console::HandleRootElementResized(StringHash eventType, VariantMap& eventData)
529 {
530 UpdateElements();
531 }
532
HandleLogMessage(StringHash eventType,VariantMap & eventData)533 void Console::HandleLogMessage(StringHash eventType, VariantMap& eventData)
534 {
535 // If printing a log message causes more messages to be logged (error accessing font), disregard them
536 if (printing_)
537 return;
538
539 using namespace LogMessage;
540
541 int level = eventData[P_LEVEL].GetInt();
542 // The message may be multi-line, so split to rows in that case
543 Vector<String> rows = eventData[P_MESSAGE].GetString().Split('\n');
544
545 for (unsigned i = 0; i < rows.Size(); ++i)
546 pendingRows_.Push(MakePair(level, rows[i]));
547
548 if (autoVisibleOnError_ && level == LOG_ERROR && !IsVisible())
549 SetVisible(true);
550 }
551
HandlePostUpdate(StringHash eventType,VariantMap & eventData)552 void Console::HandlePostUpdate(StringHash eventType, VariantMap& eventData)
553 {
554 // Ensure UI-elements are not detached
555 if (!background_->GetParent())
556 {
557 UI* ui = GetSubsystem<UI>();
558 UIElement* uiRoot = ui->GetRoot();
559 uiRoot->AddChild(background_);
560 uiRoot->AddChild(closeButton_);
561 }
562
563 if (!rowContainer_->GetNumItems() || pendingRows_.Empty())
564 return;
565
566 printing_ = true;
567 rowContainer_->DisableLayoutUpdate();
568
569 Text* text = 0;
570 for (unsigned i = 0; i < pendingRows_.Size(); ++i)
571 {
572 rowContainer_->RemoveItem((unsigned)0);
573 text = new Text(context_);
574 text->SetText(pendingRows_[i].second_);
575
576 // Highlight console messages based on their type
577 text->SetStyle(logStyles[pendingRows_[i].first_]);
578
579 rowContainer_->AddItem(text);
580 }
581
582 pendingRows_.Clear();
583
584 rowContainer_->EnsureItemVisibility(text);
585 rowContainer_->EnableLayoutUpdate();
586 rowContainer_->UpdateLayout();
587 UpdateElements(); // May need to readjust the height due to scrollbar visibility changes
588 printing_ = false;
589 }
590
591 }
592