Fix keymap.
[vuplus_xbmc] / xbmc / guilib / GUIEditControl.cpp
1 /*
2  *      Copyright (C) 2005-2013 Team XBMC
3  *      http://xbmc.org
4  *
5  *  This Program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2, or (at your option)
8  *  any later version.
9  *
10  *  This Program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, see
17  *  <http://www.gnu.org/licenses/>.
18  *
19  */
20
21 #include "GUIEditControl.h"
22 #include "GUIWindowManager.h"
23 #include "utils/CharsetConverter.h"
24 #include "GUIKeyboardFactory.h"
25 #include "dialogs/GUIDialogNumeric.h"
26 #include "input/XBMC_vkeys.h"
27 #include "Key.h"
28 #include "LocalizeStrings.h"
29 #include "XBDateTime.h"
30 #include "windowing/WindowingFactory.h"
31 #include "utils/md5.h"
32
33 #if defined(TARGET_DARWIN)
34 #include "osx/CocoaInterface.h"
35 #endif
36
37 const char* CGUIEditControl::smsLetters[10] = { " !@#$%^&*()[]{}<>/\\|0", ".,;:\'\"-+_=?`~1", "abc2", "def3", "ghi4", "jkl5", "mno6", "pqrs7", "tuv8", "wxyz9" };
38 const unsigned int CGUIEditControl::smsDelay = 1000;
39
40 using namespace std;
41
42 #ifdef TARGET_WINDOWS
43 extern HWND g_hWnd;
44 #endif
45
46 CGUIEditControl::CGUIEditControl(int parentID, int controlID, float posX, float posY,
47                                  float width, float height, const CTextureInfo &textureFocus, const CTextureInfo &textureNoFocus,
48                                  const CLabelInfo& labelInfo, const std::string &text)
49     : CGUIButtonControl(parentID, controlID, posX, posY, width, height, textureFocus, textureNoFocus, labelInfo)
50 {
51   DefaultConstructor();
52   SetLabel(text);
53 }
54
55 void CGUIEditControl::DefaultConstructor()
56 {
57   ControlType = GUICONTROL_EDIT;
58   m_textOffset = 0;
59   m_textWidth = GetWidth();
60   m_cursorPos = 0;
61   m_cursorBlink = 0;
62   m_inputHeading = 0;
63   m_inputType = INPUT_TYPE_TEXT;
64   m_smsLastKey = 0;
65   m_smsKeyIndex = 0;
66   m_label.SetAlign(m_label.GetLabelInfo().align & XBFONT_CENTER_Y); // left align
67   m_label2.GetLabelInfo().offsetX = 0;
68   m_isMD5 = false;
69   m_invalidInput = false;
70   m_inputValidator = NULL;
71   m_inputValidatorData = NULL;
72 }
73
74 CGUIEditControl::CGUIEditControl(const CGUIButtonControl &button)
75     : CGUIButtonControl(button)
76 {
77   DefaultConstructor();
78 }
79
80 CGUIEditControl::~CGUIEditControl(void)
81 {
82 }
83
84 bool CGUIEditControl::OnMessage(CGUIMessage &message)
85 {
86   if (message.GetMessage() == GUI_MSG_SET_TYPE)
87   {
88     SetInputType((INPUT_TYPE)message.GetParam1(), (int)message.GetParam2());
89     return true;
90   }
91   else if (message.GetMessage() == GUI_MSG_ITEM_SELECTED)
92   {
93     message.SetLabel(GetLabel2());
94     return true;
95   }
96   else if (message.GetMessage() == GUI_MSG_SETFOCUS ||
97            message.GetMessage() == GUI_MSG_LOSTFOCUS)
98   {
99     m_smsTimer.Stop();
100   }
101   else if (message.GetMessage() == GUI_MSG_SET_TEXT &&
102           ((message.GetControlId() <= 0 && HasFocus()) || (message.GetControlId() == GetID())))
103   {
104     SetLabel2(message.GetLabel());
105     UpdateText();
106   }
107   return CGUIButtonControl::OnMessage(message);
108 }
109
110 bool CGUIEditControl::OnAction(const CAction &action)
111 {
112   ValidateCursor();
113
114   if (m_inputType != INPUT_TYPE_READONLY)
115   {
116     if (action.GetID() == ACTION_BACKSPACE)
117     {
118       // backspace
119       if (m_cursorPos)
120       {
121         if (!ClearMD5())
122           m_text2.erase(--m_cursorPos, 1);
123         UpdateText();
124       }
125       return true;
126     }
127     else if (action.GetID() == ACTION_MOVE_LEFT)
128     {
129       if (m_cursorPos > 0)
130       {
131         m_cursorPos--;
132         UpdateText(false);
133         return true;
134       }
135     }
136     else if (action.GetID() == ACTION_MOVE_RIGHT)
137     {
138       if ((unsigned int) m_cursorPos < m_text2.size())
139       {
140         m_cursorPos++;
141         UpdateText(false);
142         return true;
143       }
144     }
145     else if (action.GetID() == ACTION_PASTE)
146     {
147       ClearMD5();
148       OnPasteClipboard();
149       return true;
150     }
151     else if (action.GetID() >= KEY_VKEY && action.GetID() < KEY_ASCII)
152     {
153       // input from the keyboard (vkey, not ascii)
154       BYTE b = action.GetID() & 0xFF;
155       if (b == XBMCVK_HOME)
156       {
157         m_cursorPos = 0;
158         UpdateText(false);
159         return true;
160       }
161       else if (b == XBMCVK_END)
162       {
163         m_cursorPos = m_text2.length();
164         UpdateText(false);
165         return true;
166       }
167       if (b == XBMCVK_LEFT && m_cursorPos > 0)
168       {
169         m_cursorPos--;
170         UpdateText(false);
171         return true;
172       }
173       if (b == XBMCVK_RIGHT && m_cursorPos < m_text2.length())
174       {
175         m_cursorPos++;
176         UpdateText(false);
177         return true;
178       }
179       if (b == XBMCVK_DELETE)
180       {
181         if (m_cursorPos < m_text2.length())
182         {
183           if (!ClearMD5())
184             m_text2.erase(m_cursorPos, 1);
185           UpdateText();
186           return true;
187         }
188       }
189       if (b == XBMCVK_BACK)
190       {
191         if (m_cursorPos > 0)
192         {
193           if (!ClearMD5())
194             m_text2.erase(--m_cursorPos, 1);
195           UpdateText();
196         }
197         return true;
198       }
199     }
200     else if (action.GetID() >= KEY_ASCII)
201     {
202       // input from the keyboard
203       switch (action.GetUnicode())
204       {
205       case '\t':
206         break;
207       case 10:
208       case 13:
209         {
210           // enter - send click message, but otherwise ignore
211           SEND_CLICK_MESSAGE(GetID(), GetParentID(), 1);
212           return true;
213         }
214       case 27:
215         { // escape - fallthrough to default action
216           return CGUIButtonControl::OnAction(action);
217         }
218       case 8:
219         {
220           // backspace
221           if (m_cursorPos)
222           {
223             if (!ClearMD5())
224               m_text2.erase(--m_cursorPos, 1);
225           }
226           break;
227         }
228       default:
229         {
230           ClearMD5();
231           m_text2.insert(m_text2.begin() + m_cursorPos++, (WCHAR)action.GetUnicode());
232           break;
233         }
234       }
235       UpdateText();
236       return true;
237     }
238     else if (action.GetID() >= REMOTE_0 && action.GetID() <= REMOTE_9)
239     { // input from the remote
240       ClearMD5();
241       if (m_inputType == INPUT_TYPE_FILTER)
242       { // filtering - use single number presses
243         m_text2.insert(m_text2.begin() + m_cursorPos++, L'0' + (action.GetID() - REMOTE_0));
244         UpdateText();
245       }
246       else
247         OnSMSCharacter(action.GetID() - REMOTE_0);
248       return true;
249     }
250   }
251   return CGUIButtonControl::OnAction(action);
252 }
253
254 void CGUIEditControl::OnClick()
255 {
256   // we received a click - it's not from the keyboard, so pop up the virtual keyboard, unless
257   // that is where we reside!
258   if (GetParentID() == WINDOW_DIALOG_KEYBOARD)
259     return;
260
261   CStdString utf8;
262   g_charsetConverter.wToUTF8(m_text2, utf8);
263   bool textChanged = false;
264   CStdString heading = g_localizeStrings.Get(m_inputHeading ? m_inputHeading : 16028);
265   switch (m_inputType)
266   {
267     case INPUT_TYPE_READONLY:
268       textChanged = false;
269       break;
270     case INPUT_TYPE_NUMBER:
271       textChanged = CGUIDialogNumeric::ShowAndGetNumber(utf8, heading);
272       break;
273     case INPUT_TYPE_SECONDS:
274       textChanged = CGUIDialogNumeric::ShowAndGetSeconds(utf8, g_localizeStrings.Get(21420));
275       break;
276     case INPUT_TYPE_TIME:
277     {
278       CDateTime dateTime;
279       dateTime.SetFromDBTime(utf8);
280       SYSTEMTIME time;
281       dateTime.GetAsSystemTime(time);
282       if (CGUIDialogNumeric::ShowAndGetTime(time, heading > 0 ? heading : g_localizeStrings.Get(21420)))
283       {
284         dateTime = CDateTime(time);
285         utf8 = dateTime.GetAsLocalizedTime("", false);
286         textChanged = true;
287       }
288       break;
289     }
290     case INPUT_TYPE_DATE:
291     {
292       CDateTime dateTime;
293       dateTime.SetFromDBDate(utf8);
294       if (dateTime < CDateTime(2000,1, 1, 0, 0, 0))
295         dateTime = CDateTime(2000, 1, 1, 0, 0, 0);
296       SYSTEMTIME date;
297       dateTime.GetAsSystemTime(date);
298       if (CGUIDialogNumeric::ShowAndGetDate(date, heading > 0 ? heading : g_localizeStrings.Get(21420)))
299       {
300         dateTime = CDateTime(date);
301         utf8 = dateTime.GetAsDBDate();
302         textChanged = true;
303       }
304       break;
305     }
306     case INPUT_TYPE_IPADDRESS:
307       textChanged = CGUIDialogNumeric::ShowAndGetIPAddress(utf8, heading);
308       break;
309     case INPUT_TYPE_SEARCH:
310       textChanged = CGUIKeyboardFactory::ShowAndGetFilter(utf8, true);
311       break;
312     case INPUT_TYPE_FILTER:
313       textChanged = CGUIKeyboardFactory::ShowAndGetFilter(utf8, false);
314       break;
315     case INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW:
316       textChanged = CGUIDialogNumeric::ShowAndVerifyNewPassword(utf8);
317       break;
318     case INPUT_TYPE_PASSWORD_MD5:
319       utf8 = ""; // TODO: Ideally we'd send this to the keyboard and tell the keyboard we have this type of input
320       // fallthrough
321     case INPUT_TYPE_TEXT:
322     default:
323       textChanged = CGUIKeyboardFactory::ShowAndGetInput(utf8, heading, true, m_inputType == INPUT_TYPE_PASSWORD || m_inputType == INPUT_TYPE_PASSWORD_MD5);
324       break;
325   }
326   if (textChanged)
327   {
328     ClearMD5();
329     g_charsetConverter.utf8ToW(utf8, m_text2);
330     m_cursorPos = m_text2.size();
331     UpdateText();
332     m_cursorPos = m_text2.size();
333   }
334 }
335
336 void CGUIEditControl::UpdateText(bool sendUpdate)
337 {
338   m_smsTimer.Stop();
339   if (sendUpdate)
340   {
341     ValidateInput();
342
343     SEND_CLICK_MESSAGE(GetID(), GetParentID(), 0);
344
345     m_textChangeActions.ExecuteActions(GetID(), GetParentID());
346   }
347   SetInvalid();
348 }
349
350 void CGUIEditControl::SetInputType(CGUIEditControl::INPUT_TYPE type, int heading)
351 {
352   m_inputType = type;
353   m_inputHeading = heading;
354   // TODO: Verify the current input string?
355 }
356
357 void CGUIEditControl::RecalcLabelPosition()
358 {
359   // ensure that our cursor is within our width
360   ValidateCursor();
361
362   CStdStringW text = GetDisplayedText();
363   m_textWidth = m_label.CalcTextWidth(text + L'|');
364   float beforeCursorWidth = m_label.CalcTextWidth(text.substr(0, m_cursorPos));
365   float afterCursorWidth = m_label.CalcTextWidth(text.substr(0, m_cursorPos) + L'|');
366   float leftTextWidth = m_label.GetRenderRect().Width();
367   float maxTextWidth = m_label.GetMaxWidth();
368   if (leftTextWidth > 0)
369     maxTextWidth -= leftTextWidth + spaceWidth;
370
371   // if skinner forgot to set height :p
372   if (m_height == 0 && m_label.GetLabelInfo().font)
373     m_height = m_label.GetLabelInfo().font->GetTextHeight(1);
374
375   if (m_textWidth > maxTextWidth)
376   { // we render taking up the full width, so make sure our cursor position is
377     // within the render window
378     if (m_textOffset + afterCursorWidth > maxTextWidth)
379     {
380       // move the position to the left (outside of the viewport)
381       m_textOffset = maxTextWidth - afterCursorWidth;
382     }
383     else if (m_textOffset + beforeCursorWidth < 0) // offscreen to the left
384     {
385       // otherwise use original position
386       m_textOffset = -beforeCursorWidth;
387     }
388     else if (m_textOffset + m_textWidth < maxTextWidth)
389     { // we have more text than we're allowed, but we aren't filling all the space
390       m_textOffset = maxTextWidth - m_textWidth;
391     }
392   }
393   else
394     m_textOffset = 0;
395 }
396
397 void CGUIEditControl::ProcessText(unsigned int currentTime)
398 {
399   if (m_smsTimer.GetElapsedMilliseconds() > smsDelay)
400     UpdateText();
401
402   if (m_bInvalidated)
403   {
404     m_label.SetMaxRect(m_posX, m_posY, m_width, m_height);
405     m_label.SetText(m_info.GetLabel(GetParentID()));
406     RecalcLabelPosition();
407   }
408
409   bool changed = false;
410
411   m_clipRect.x1 = m_label.GetRenderRect().x1;
412   m_clipRect.x2 = m_clipRect.x1 + m_label.GetMaxWidth();
413   m_clipRect.y1 = m_posY;
414   m_clipRect.y2 = m_posY + m_height;
415
416   // start by rendering the normal text
417   float leftTextWidth = m_label.GetRenderRect().Width();
418   if (leftTextWidth > 0)
419   {
420     // render the text on the left
421     changed |= m_label.SetColor(GetTextColor());
422     changed |= m_label.Process(currentTime);
423
424     m_clipRect.x1 += leftTextWidth + spaceWidth;
425   }
426
427   if (g_graphicsContext.SetClipRegion(m_clipRect.x1, m_clipRect.y1, m_clipRect.Width(), m_clipRect.Height()))
428   {
429     uint32_t align = m_label.GetLabelInfo().align & XBFONT_CENTER_Y; // start aligned left
430     if (m_label2.GetTextWidth() < m_clipRect.Width())
431     { // align text as our text fits
432       if (leftTextWidth > 0)
433       { // right align as we have 2 labels
434         align |= XBFONT_RIGHT;
435       }
436       else
437       { // align by whatever the skinner requests
438         align |= (m_label2.GetLabelInfo().align & 3);
439       }
440     }
441     CStdStringW text = GetDisplayedText();
442     // add the cursor if we're focused
443     if (HasFocus() && m_inputType != INPUT_TYPE_READONLY)
444     {
445       CStdStringW col;
446       if ((m_focusCounter % 64) > 32)
447         col = L"|";
448       else
449         col = L"[COLOR 00FFFFFF]|[/COLOR]";
450       text.insert(m_cursorPos, col);
451     }
452
453     changed |= m_label2.SetMaxRect(m_clipRect.x1 + m_textOffset, m_posY, m_clipRect.Width() - m_textOffset, m_height);
454     if (text.empty())
455       changed |= m_label2.SetText(m_hintInfo.GetLabel(GetParentID()));
456     else
457       changed |= m_label2.SetTextW(text);
458     changed |= m_label2.SetAlign(align);
459     changed |= m_label2.SetColor(GetTextColor());
460     changed |= m_label2.SetOverflow(CGUILabel::OVER_FLOW_CLIP);
461     changed |= m_label2.Process(currentTime);
462     g_graphicsContext.RestoreClipRegion();
463   }
464   if (changed)
465     MarkDirtyRegion();
466 }
467
468 void CGUIEditControl::RenderText()
469 {
470   m_label.Render();
471
472   if (g_graphicsContext.SetClipRegion(m_clipRect.x1, m_clipRect.y1, m_clipRect.Width(), m_clipRect.Height()))
473   {
474     m_label2.Render();
475     g_graphicsContext.RestoreClipRegion();
476   }
477 }
478
479 CGUILabel::COLOR CGUIEditControl::GetTextColor() const
480 {
481   CGUILabel::COLOR color = CGUIButtonControl::GetTextColor();
482   if (color != CGUILabel::COLOR_DISABLED && HasInvalidInput())
483     return CGUILabel::COLOR_INVALID;
484
485   return color;
486 }
487
488 void CGUIEditControl::SetHint(const CGUIInfoLabel& hint)
489 {
490   m_hintInfo = hint;
491 }
492
493 CStdStringW CGUIEditControl::GetDisplayedText() const
494 {
495   if (m_inputType == INPUT_TYPE_PASSWORD || m_inputType == INPUT_TYPE_PASSWORD_MD5 || m_inputType == INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW)
496   {
497     CStdStringW text;
498     text.append(m_text2.size(), L'*');
499     return text;
500   }
501   return m_text2;
502 }
503
504 void CGUIEditControl::ValidateCursor()
505 {
506   if (m_cursorPos > m_text2.size())
507     m_cursorPos = m_text2.size();
508 }
509
510 void CGUIEditControl::SetLabel(const std::string &text)
511 {
512   CGUIButtonControl::SetLabel(text);
513   SetInvalid();
514 }
515
516 void CGUIEditControl::SetLabel2(const std::string &text)
517 {
518   CStdStringW newText;
519   g_charsetConverter.utf8ToW(text, newText);
520   if (newText != m_text2)
521   {
522     m_isMD5 = (m_inputType == INPUT_TYPE_PASSWORD_MD5 || m_inputType == INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW);
523     m_text2 = newText;
524     m_cursorPos = m_text2.size();
525     ValidateInput();
526     SetInvalid();
527   }
528 }
529
530 CStdString CGUIEditControl::GetLabel2() const
531 {
532   CStdString text;
533   g_charsetConverter.wToUTF8(m_text2, text);
534   if (m_inputType == INPUT_TYPE_PASSWORD_MD5 && !m_isMD5)
535     return XBMC::XBMC_MD5::GetMD5(text);
536   return text;
537 }
538
539 bool CGUIEditControl::ClearMD5()
540 {
541   if (!(m_inputType == INPUT_TYPE_PASSWORD_MD5 || m_inputType == INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW) || !m_isMD5)
542     return false;
543   
544   m_text2.clear();
545   m_cursorPos = 0;
546   if (m_inputType != INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW)
547     m_isMD5 = false;
548   return true;
549 }
550
551 unsigned int CGUIEditControl::GetCursorPosition() const
552 {
553   return m_cursorPos;
554 }
555
556 void CGUIEditControl::SetCursorPosition(unsigned int iPosition)
557 {
558   m_cursorPos = iPosition;
559 }
560
561 void CGUIEditControl::OnSMSCharacter(unsigned int key)
562 {
563   assert(key < 10);
564   bool sendUpdate = false;
565   if (m_smsTimer.IsRunning())
566   {
567     // we're already entering an SMS character
568     if (key != m_smsLastKey || m_smsTimer.GetElapsedMilliseconds() > smsDelay)
569     { // a different key was clicked than last time, or we have timed out
570       m_smsLastKey = key;
571       m_smsKeyIndex = 0;
572       sendUpdate = true;
573     }
574     else
575     { // same key as last time within the appropriate time period
576       m_smsKeyIndex++;
577       if (m_cursorPos)
578         m_text2.erase(--m_cursorPos, 1);
579     }
580   }
581   else
582   { // key is pressed for the first time
583     m_smsLastKey = key;
584     m_smsKeyIndex = 0;
585   }
586
587   m_smsKeyIndex = m_smsKeyIndex % strlen(smsLetters[key]);
588
589   m_text2.insert(m_text2.begin() + m_cursorPos++, smsLetters[key][m_smsKeyIndex]);
590   UpdateText(sendUpdate);
591   m_smsTimer.StartZero();
592 }
593
594 void CGUIEditControl::OnPasteClipboard()
595 {
596   CStdStringW unicode_text;
597   CStdStringA utf8_text;
598
599 // Get text from the clipboard
600   utf8_text = g_Windowing.GetClipboardText();
601   g_charsetConverter.utf8ToW(utf8_text, unicode_text);
602
603   // Insert the pasted text at the current cursor position.
604   if (unicode_text.length() > 0)
605   {
606     CStdStringW left_end = m_text2.substr(0, m_cursorPos);
607     CStdStringW right_end = m_text2.substr(m_cursorPos);
608
609     m_text2 = left_end;
610     m_text2.append(unicode_text);
611     m_text2.append(right_end);
612     m_cursorPos += unicode_text.length();
613     UpdateText();
614   }
615 }
616
617 void CGUIEditControl::SetInputValidation(StringValidation::Validator inputValidator, void *data /* = NULL */)
618 {
619   if (m_inputValidator == inputValidator)
620     return;
621   
622   m_inputValidator = inputValidator;
623   m_inputValidatorData = data;
624   // the input validator has changed, so re-validate the current data
625   ValidateInput();
626 }
627
628 bool CGUIEditControl::ValidateInput(const CStdStringW &data) const
629 {
630   if (m_inputValidator == NULL)
631     return true;
632
633   return m_inputValidator(GetLabel2(), (void*)(m_inputValidatorData != NULL ? m_inputValidatorData : this));
634 }
635
636 void CGUIEditControl::ValidateInput()
637 {
638   // validate the input
639   bool invalid = !ValidateInput(m_text2);
640   // nothing to do if still valid/invalid
641   if (invalid != m_invalidInput)
642   {
643     // the validity state has changed so we need to update the control
644     m_invalidInput = invalid;
645
646     // let the window/dialog know that the validity has changed
647     CGUIMessage msg(GUI_MSG_VALIDITY_CHANGED, GetID(), GetID(), m_invalidInput ? 0 : 1);
648     SendWindowMessage(msg);
649
650     SetInvalid();
651   }
652 }