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