diff options
Diffstat (limited to 'xbmc/guilib/GUIEditControl.cpp')
-rw-r--r-- | xbmc/guilib/GUIEditControl.cpp | 743 |
1 files changed, 743 insertions, 0 deletions
diff --git a/xbmc/guilib/GUIEditControl.cpp b/xbmc/guilib/GUIEditControl.cpp new file mode 100644 index 0000000..325a703 --- /dev/null +++ b/xbmc/guilib/GUIEditControl.cpp @@ -0,0 +1,743 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIEditControl.h" + +#include "GUIFont.h" +#include "GUIKeyboardFactory.h" +#include "GUIUserMessages.h" +#include "GUIWindowManager.h" +#include "LocalizeStrings.h" +#include "ServiceBroker.h" +#include "XBDateTime.h" +#include "dialogs/GUIDialogNumeric.h" +#include "input/Key.h" +#include "input/XBMC_vkeys.h" +#include "utils/CharsetConverter.h" +#include "utils/ColorUtils.h" +#include "utils/Digest.h" +#include "utils/Variant.h" +#include "windowing/WinSystem.h" + +using namespace KODI::GUILIB; + +using KODI::UTILITY::CDigest; + +const char* CGUIEditControl::smsLetters[10] = { " !@#$%^&*()[]{}<>/\\|0", ".,;:\'\"-+_=?`~1", "abc2ABC", "def3DEF", "ghi4GHI", "jkl5JKL", "mno6MNO", "pqrs7PQRS", "tuv8TUV", "wxyz9WXYZ" }; +const unsigned int CGUIEditControl::smsDelay = 1000; + +#ifdef TARGET_WINDOWS +extern HWND g_hWnd; +#endif + +CGUIEditControl::CGUIEditControl(int parentID, int controlID, float posX, float posY, + float width, float height, const CTextureInfo &textureFocus, const CTextureInfo &textureNoFocus, + const CLabelInfo& labelInfo, const std::string &text) + : CGUIButtonControl(parentID, controlID, posX, posY, width, height, textureFocus, textureNoFocus, labelInfo) +{ + DefaultConstructor(); + SetLabel(text); +} + +void CGUIEditControl::DefaultConstructor() +{ + ControlType = GUICONTROL_EDIT; + m_textOffset = 0; + m_textWidth = GetWidth(); + m_cursorPos = 0; + m_cursorBlink = 0; + m_inputHeading = g_localizeStrings.Get(16028); + m_inputType = INPUT_TYPE_TEXT; + m_smsLastKey = 0; + m_smsKeyIndex = 0; + m_label.SetAlign(m_label.GetLabelInfo().align & XBFONT_CENTER_Y); // left align + m_label2.GetLabelInfo().offsetX = 0; + m_isMD5 = false; + m_invalidInput = false; + m_inputValidator = NULL; + m_inputValidatorData = NULL; + m_editLength = 0; + m_editOffset = 0; +} + +CGUIEditControl::CGUIEditControl(const CGUIButtonControl &button) + : CGUIButtonControl(button) +{ + DefaultConstructor(); +} + +CGUIEditControl::~CGUIEditControl(void) = default; + +bool CGUIEditControl::OnMessage(CGUIMessage &message) +{ + if (message.GetMessage() == GUI_MSG_SET_TYPE) + { + SetInputType((INPUT_TYPE)message.GetParam1(), message.GetParam2()); + return true; + } + else if (message.GetMessage() == GUI_MSG_ITEM_SELECTED) + { + message.SetLabel(GetLabel2()); + return true; + } + else if (message.GetMessage() == GUI_MSG_SET_TEXT && + ((message.GetControlId() <= 0 && HasFocus()) || (message.GetControlId() == GetID()))) + { + SetLabel2(message.GetLabel()); + UpdateText(); + } + return CGUIButtonControl::OnMessage(message); +} + +bool CGUIEditControl::OnAction(const CAction &action) +{ + ValidateCursor(); + + if (m_inputType != INPUT_TYPE_READONLY) + { + if (action.GetID() == ACTION_BACKSPACE) + { + // backspace + if (m_cursorPos) + { + if (!ClearMD5()) + m_text2.erase(--m_cursorPos, 1); + UpdateText(); + } + return true; + } + else if (action.GetID() == ACTION_MOVE_LEFT || + action.GetID() == ACTION_CURSOR_LEFT) + { + if (m_cursorPos > 0) + { + m_cursorPos--; + UpdateText(false); + return true; + } + } + else if (action.GetID() == ACTION_MOVE_RIGHT || + action.GetID() == ACTION_CURSOR_RIGHT) + { + if (m_cursorPos < m_text2.size()) + { + m_cursorPos++; + UpdateText(false); + return true; + } + } + else if (action.GetID() == ACTION_PASTE) + { + ClearMD5(); + OnPasteClipboard(); + return true; + } + else if (action.GetID() >= KEY_VKEY && action.GetID() < KEY_UNICODE && m_edit.empty()) + { + // input from the keyboard (vkey, not ascii) + unsigned char b = action.GetID() & 0xFF; + if (b == XBMCVK_HOME) + { + m_cursorPos = 0; + UpdateText(false); + return true; + } + else if (b == XBMCVK_END) + { + m_cursorPos = m_text2.length(); + UpdateText(false); + return true; + } + if (b == XBMCVK_LEFT && m_cursorPos > 0) + { + m_cursorPos--; + UpdateText(false); + return true; + } + if (b == XBMCVK_RIGHT && m_cursorPos < m_text2.length()) + { + m_cursorPos++; + UpdateText(false); + return true; + } + if (b == XBMCVK_DELETE) + { + if (m_cursorPos < m_text2.length()) + { + if (!ClearMD5()) + m_text2.erase(m_cursorPos, 1); + UpdateText(); + return true; + } + } + if (b == XBMCVK_BACK) + { + if (m_cursorPos > 0) + { + if (!ClearMD5()) + m_text2.erase(--m_cursorPos, 1); + UpdateText(); + } + return true; + } + else if (b == XBMCVK_RETURN || b == XBMCVK_NUMPADENTER) + { + // enter - send click message, but otherwise ignore + SEND_CLICK_MESSAGE(GetID(), GetParentID(), 1); + return true; + } + else if (b == XBMCVK_ESCAPE) + { // escape - fallthrough to default action + return CGUIButtonControl::OnAction(action); + } + } + else if (action.GetID() == KEY_UNICODE) + { + // input from the keyboard + int ch = action.GetUnicode(); + // ignore non-printing characters + if ( !((0 <= ch && ch < 0x8) || (0xE <= ch && ch < 0x1B) || (0x1C <= ch && ch < 0x20)) ) + { + switch (ch) + { + case 9: // tab, ignore + case 11: // Non-printing character, ignore + case 12: // Non-printing character, ignore + break; + case 10: + case 13: + { + // enter - send click message, but otherwise ignore + SEND_CLICK_MESSAGE(GetID(), GetParentID(), 1); + return true; + } + case 27: + { // escape - fallthrough to default action + return CGUIButtonControl::OnAction(action); + } + case 8: + { + // backspace + if (m_cursorPos) + { + if (!ClearMD5()) + m_text2.erase(--m_cursorPos, 1); + } + break; + } + case 127: + { // delete + if (m_cursorPos < m_text2.length()) + { + if (!ClearMD5()) + m_text2.erase(m_cursorPos, 1); + } + break; + } + default: + { + ClearMD5(); + m_edit.clear(); + m_text2.insert(m_text2.begin() + m_cursorPos++, action.GetUnicode()); + break; + } + } + UpdateText(); + return true; + } + } + else if (action.GetID() >= REMOTE_0 && action.GetID() <= REMOTE_9) + { // input from the remote + ClearMD5(); + m_edit.clear(); + OnSMSCharacter(action.GetID() - REMOTE_0); + return true; + } + else if (action.GetID() == ACTION_INPUT_TEXT) + { + m_edit.clear(); + std::wstring str; + g_charsetConverter.utf8ToW(action.GetText(), str, false); + m_text2.insert(m_cursorPos, str); + m_cursorPos += str.size(); + UpdateText(); + return true; + } + } + return CGUIButtonControl::OnAction(action); +} + +void CGUIEditControl::OnClick() +{ + // we received a click - it's not from the keyboard, so pop up the virtual keyboard, unless + // that is where we reside! + if (GetParentID() == WINDOW_DIALOG_KEYBOARD) + return; + + std::string utf8; + g_charsetConverter.wToUTF8(m_text2, utf8); + bool textChanged = false; + switch (m_inputType) + { + case INPUT_TYPE_READONLY: + textChanged = false; + break; + case INPUT_TYPE_NUMBER: + textChanged = CGUIDialogNumeric::ShowAndGetNumber(utf8, m_inputHeading); + break; + case INPUT_TYPE_SECONDS: + textChanged = CGUIDialogNumeric::ShowAndGetSeconds(utf8, g_localizeStrings.Get(21420)); + break; + case INPUT_TYPE_TIME: + { + CDateTime dateTime; + dateTime.SetFromDBTime(utf8); + KODI::TIME::SystemTime time; + dateTime.GetAsSystemTime(time); + if (CGUIDialogNumeric::ShowAndGetTime(time, !m_inputHeading.empty() ? m_inputHeading : g_localizeStrings.Get(21420))) + { + dateTime = CDateTime(time); + utf8 = dateTime.GetAsLocalizedTime("", false); + textChanged = true; + } + break; + } + case INPUT_TYPE_DATE: + { + CDateTime dateTime; + dateTime.SetFromDBDate(utf8); + if (dateTime < CDateTime(2000,1, 1, 0, 0, 0)) + dateTime = CDateTime(2000, 1, 1, 0, 0, 0); + KODI::TIME::SystemTime date; + dateTime.GetAsSystemTime(date); + if (CGUIDialogNumeric::ShowAndGetDate(date, !m_inputHeading.empty() ? m_inputHeading : g_localizeStrings.Get(21420))) + { + dateTime = CDateTime(date); + utf8 = dateTime.GetAsDBDate(); + textChanged = true; + } + break; + } + case INPUT_TYPE_IPADDRESS: + textChanged = CGUIDialogNumeric::ShowAndGetIPAddress(utf8, m_inputHeading); + break; + case INPUT_TYPE_SEARCH: + textChanged = CGUIKeyboardFactory::ShowAndGetFilter(utf8, true); + break; + case INPUT_TYPE_FILTER: + textChanged = CGUIKeyboardFactory::ShowAndGetFilter(utf8, false); + break; + case INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW: + textChanged = CGUIDialogNumeric::ShowAndVerifyNewPassword(utf8); + break; + case INPUT_TYPE_PASSWORD_MD5: + utf8 = ""; //! @todo Ideally we'd send this to the keyboard and tell the keyboard we have this type of input + // fallthrough + [[fallthrough]]; + case INPUT_TYPE_TEXT: + default: + textChanged = CGUIKeyboardFactory::ShowAndGetInput(utf8, m_inputHeading, true, m_inputType == INPUT_TYPE_PASSWORD || m_inputType == INPUT_TYPE_PASSWORD_MD5); + break; + } + if (textChanged) + { + ClearMD5(); + m_edit.clear(); + g_charsetConverter.utf8ToW(utf8, m_text2, false); + m_cursorPos = m_text2.size(); + UpdateText(); + m_cursorPos = m_text2.size(); + } +} + +void CGUIEditControl::UpdateText(bool sendUpdate) +{ + m_smsTimer.Stop(); + if (sendUpdate) + { + ValidateInput(); + + SEND_CLICK_MESSAGE(GetID(), GetParentID(), 0); + + m_textChangeActions.ExecuteActions(GetID(), GetParentID()); + } + SetInvalid(); +} + +void CGUIEditControl::SetInputType(CGUIEditControl::INPUT_TYPE type, const CVariant& heading) +{ + m_inputType = type; + if (heading.isString()) + m_inputHeading = heading.asString(); + else if (heading.isInteger() && heading.asInteger()) + m_inputHeading = g_localizeStrings.Get(static_cast<uint32_t>(heading.asInteger())); + //! @todo Verify the current input string? +} + +void CGUIEditControl::RecalcLabelPosition() +{ + // ensure that our cursor is within our width + ValidateCursor(); + + std::wstring text = GetDisplayedText(); + m_textWidth = m_label.CalcTextWidth(text + L'|'); + float beforeCursorWidth = m_label.CalcTextWidth(text.substr(0, m_cursorPos)); + float afterCursorWidth = m_label.CalcTextWidth(text.substr(0, m_cursorPos) + L'|'); + float leftTextWidth = m_label.GetRenderRect().Width(); + float maxTextWidth = m_label.GetMaxWidth(); + if (leftTextWidth > 0) + maxTextWidth -= leftTextWidth + spaceWidth; + + // if skinner forgot to set height :p + if (m_height == 0 && m_label.GetLabelInfo().font) + m_height = m_label.GetLabelInfo().font->GetTextHeight(1); + + if (m_textWidth > maxTextWidth) + { // we render taking up the full width, so make sure our cursor position is + // within the render window + if (m_textOffset + afterCursorWidth > maxTextWidth) + { + // move the position to the left (outside of the viewport) + m_textOffset = maxTextWidth - afterCursorWidth; + } + else if (m_textOffset + beforeCursorWidth < 0) // offscreen to the left + { + // otherwise use original position + m_textOffset = -beforeCursorWidth; + } + else if (m_textOffset + m_textWidth < maxTextWidth) + { // we have more text than we're allowed, but we aren't filling all the space + m_textOffset = maxTextWidth - m_textWidth; + } + } + else + m_textOffset = 0; +} + +void CGUIEditControl::ProcessText(unsigned int currentTime) +{ + if (m_smsTimer.IsRunning() && m_smsTimer.GetElapsedMilliseconds() > smsDelay) + UpdateText(); + + if (m_bInvalidated) + { + m_label.SetMaxRect(m_posX, m_posY, m_width, m_height); + m_label.SetText(m_info.GetLabel(GetParentID())); + RecalcLabelPosition(); + } + + bool changed = false; + + m_clipRect.x1 = m_label.GetRenderRect().x1; + m_clipRect.x2 = m_clipRect.x1 + m_label.GetMaxWidth(); + m_clipRect.y1 = m_posY; + m_clipRect.y2 = m_posY + m_height; + + // start by rendering the normal text + float leftTextWidth = m_label.GetRenderRect().Width(); + if (leftTextWidth > 0) + { + // render the text on the left + changed |= m_label.SetColor(GetTextColor()); + changed |= m_label.Process(currentTime); + + m_clipRect.x1 += leftTextWidth + spaceWidth; + } + + if (CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_clipRect.x1, m_clipRect.y1, m_clipRect.Width(), m_clipRect.Height())) + { + uint32_t align = m_label.GetLabelInfo().align & XBFONT_CENTER_Y; // start aligned left + if (m_label2.GetTextWidth() < m_clipRect.Width()) + { // align text as our text fits + if (leftTextWidth > 0) + { // right align as we have 2 labels + align |= XBFONT_RIGHT; + } + else + { // align by whatever the skinner requests + align |= (m_label2.GetLabelInfo().align & 3); + } + } + changed |= m_label2.SetMaxRect(m_clipRect.x1 + m_textOffset, m_posY, m_clipRect.Width() - m_textOffset, m_height); + + std::wstring text = GetDisplayedText(); + std::string hint = m_hintInfo.GetLabel(GetParentID()); + + if (!HasFocus() && text.empty() && !hint.empty()) + { + changed |= m_label2.SetText(hint); + } + else if ((HasFocus() || GetParentID() == WINDOW_DIALOG_KEYBOARD) && + m_inputType != INPUT_TYPE_READONLY) + { + changed |= SetStyledText(text); + } + else + changed |= m_label2.SetTextW(text); + + changed |= m_label2.SetAlign(align); + changed |= m_label2.SetColor(GetTextColor()); + changed |= m_label2.SetOverflow(CGUILabel::OVER_FLOW_CLIP); + changed |= m_label2.Process(currentTime); + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); + } + if (changed) + MarkDirtyRegion(); +} + +void CGUIEditControl::RenderText() +{ + m_label.Render(); + + if (CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_clipRect.x1, m_clipRect.y1, m_clipRect.Width(), m_clipRect.Height())) + { + m_label2.Render(); + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); + } +} + +CGUILabel::COLOR CGUIEditControl::GetTextColor() const +{ + CGUILabel::COLOR color = CGUIButtonControl::GetTextColor(); + if (color != CGUILabel::COLOR_DISABLED && HasInvalidInput()) + return CGUILabel::COLOR_INVALID; + + return color; +} + +void CGUIEditControl::SetHint(const GUIINFO::CGUIInfoLabel& hint) +{ + m_hintInfo = hint; +} + +std::wstring CGUIEditControl::GetDisplayedText() const +{ + std::wstring text(m_text2); + if (m_inputType == INPUT_TYPE_PASSWORD || m_inputType == INPUT_TYPE_PASSWORD_MD5 || m_inputType == INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW) + { + text.clear(); + if (m_smsTimer.IsRunning()) + { // using the remove to input, so display the last key input + text.append(m_cursorPos - 1, L'*'); + text.append(1, m_text2[m_cursorPos - 1]); + text.append(m_text2.size() - m_cursorPos, L'*'); + } + else + text.append(m_text2.size(), L'*'); + } + else if (!m_edit.empty()) + text.insert(m_editOffset, m_edit); + return text; +} + +bool CGUIEditControl::SetStyledText(const std::wstring &text) +{ + vecText styled; + styled.reserve(text.size() + 1); + + std::vector<UTILS::COLOR::Color> colors; + colors.push_back(m_label.GetLabelInfo().textColor); + colors.push_back(m_label.GetLabelInfo().disabledColor); + UTILS::COLOR::Color select = m_label.GetLabelInfo().selectedColor; + if (!select) + select = 0xFFFF0000; + colors.push_back(select); + colors.push_back(0x00FFFFFF); + + unsigned int startHighlight = m_cursorPos; + unsigned int endHighlight = m_cursorPos + m_edit.size(); + unsigned int startSelection = m_cursorPos + m_editOffset; + unsigned int endSelection = m_cursorPos + m_editOffset + m_editLength; + + CGUIFont* font = m_label2.GetLabelInfo().font; + uint32_t style = (font ? font->GetStyle() : (FONT_STYLE_NORMAL & FONT_STYLE_MASK)) << 24; + + for (unsigned int i = 0; i < text.size(); i++) + { + uint32_t ch = text[i] | style; + if (m_editLength > 0 && startSelection <= i && i < endSelection) + ch |= (2 << 16); // highlight the letters we're playing with + else if (!m_edit.empty() && (i < startHighlight || i >= endHighlight)) + ch |= (1 << 16); // dim the bits we're not editing + styled.push_back(ch); + } + + // show the cursor + uint32_t ch = L'|' | style; + if ((++m_cursorBlink % 64) > 32) + ch |= (3 << 16); + styled.insert(styled.begin() + m_cursorPos, ch); + + return m_label2.SetStyledText(styled, colors); +} + +void CGUIEditControl::ValidateCursor() +{ + if (m_cursorPos > m_text2.size()) + m_cursorPos = m_text2.size(); +} + +void CGUIEditControl::SetLabel(const std::string &text) +{ + CGUIButtonControl::SetLabel(text); + SetInvalid(); +} + +void CGUIEditControl::SetLabel2(const std::string &text) +{ + m_edit.clear(); + std::wstring newText; + g_charsetConverter.utf8ToW(text, newText, false); + if (newText != m_text2) + { + m_isMD5 = (m_inputType == INPUT_TYPE_PASSWORD_MD5 || m_inputType == INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW); + m_text2 = newText; + m_cursorPos = m_text2.size(); + ValidateInput(); + SetInvalid(); + } +} + +std::string CGUIEditControl::GetLabel2() const +{ + std::string text; + g_charsetConverter.wToUTF8(m_text2, text); + if (m_inputType == INPUT_TYPE_PASSWORD_MD5 && !m_isMD5) + return CDigest::Calculate(CDigest::Type::MD5, text); + return text; +} + +bool CGUIEditControl::ClearMD5() +{ + if (!(m_inputType == INPUT_TYPE_PASSWORD_MD5 || m_inputType == INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW) || !m_isMD5) + return false; + + m_text2.clear(); + m_cursorPos = 0; + if (m_inputType != INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW) + m_isMD5 = false; + return true; +} + +unsigned int CGUIEditControl::GetCursorPosition() const +{ + return m_cursorPos; +} + +void CGUIEditControl::SetCursorPosition(unsigned int iPosition) +{ + m_cursorPos = iPosition; +} + +void CGUIEditControl::OnSMSCharacter(unsigned int key) +{ + assert(key < 10); + if (m_smsTimer.IsRunning()) + { + // we're already entering an SMS character + if (key != m_smsLastKey || m_smsTimer.GetElapsedMilliseconds() > smsDelay) + { // a different key was clicked than last time, or we have timed out + m_smsLastKey = key; + m_smsKeyIndex = 0; + } + else + { // same key as last time within the appropriate time period + m_smsKeyIndex++; + if (m_cursorPos) + m_text2.erase(--m_cursorPos, 1); + } + } + else + { // key is pressed for the first time + m_smsLastKey = key; + m_smsKeyIndex = 0; + } + + m_smsKeyIndex = m_smsKeyIndex % strlen(smsLetters[key]); + + m_text2.insert(m_text2.begin() + m_cursorPos++, smsLetters[key][m_smsKeyIndex]); + UpdateText(); + m_smsTimer.StartZero(); +} + +void CGUIEditControl::OnPasteClipboard() +{ + std::wstring unicode_text; + std::string utf8_text; + + // Get text from the clipboard + utf8_text = CServiceBroker::GetWinSystem()->GetClipboardText(); + g_charsetConverter.utf8ToW(utf8_text, unicode_text, false); + + // Insert the pasted text at the current cursor position. + if (unicode_text.length() > 0) + { + std::wstring left_end = m_text2.substr(0, m_cursorPos); + std::wstring right_end = m_text2.substr(m_cursorPos); + + m_text2 = left_end; + m_text2.append(unicode_text); + m_text2.append(right_end); + m_cursorPos += unicode_text.length(); + UpdateText(); + } +} + +void CGUIEditControl::SetInputValidation(StringValidation::Validator inputValidator, void *data /* = NULL */) +{ + if (m_inputValidator == inputValidator) + return; + + m_inputValidator = inputValidator; + m_inputValidatorData = data; + // the input validator has changed, so re-validate the current data + ValidateInput(); +} + +bool CGUIEditControl::ValidateInput(const std::wstring &data) const +{ + if (m_inputValidator == NULL) + return true; + + return m_inputValidator(GetLabel2(), m_inputValidatorData != NULL ? m_inputValidatorData : const_cast<void*>((const void*)this)); +} + +void CGUIEditControl::ValidateInput() +{ + // validate the input + bool invalid = !ValidateInput(m_text2); + // nothing to do if still valid/invalid + if (invalid != m_invalidInput) + { + // the validity state has changed so we need to update the control + m_invalidInput = invalid; + + // let the window/dialog know that the validity has changed + CGUIMessage msg(GUI_MSG_VALIDITY_CHANGED, GetID(), GetID(), m_invalidInput ? 0 : 1); + SendWindowMessage(msg); + + SetInvalid(); + } +} + +void CGUIEditControl::SetFocus(bool focus) +{ + m_smsTimer.Stop(); + CGUIControl::SetFocus(focus); + SetInvalid(); +} + +std::string CGUIEditControl::GetDescriptionByIndex(int index) const +{ + if (index == 0) + return GetDescription(); + else if(index == 1) + return GetLabel2(); + + return ""; +} |