Index: ps/trunk/source/gui/ObjectTypes/CInput.cpp =================================================================== --- ps/trunk/source/gui/ObjectTypes/CInput.cpp (revision 24644) +++ ps/trunk/source/gui/ObjectTypes/CInput.cpp (revision 24645) @@ -1,2105 +1,2105 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "CInput.h" #include "graphics/FontMetrics.h" #include "graphics/ShaderManager.h" #include "graphics/TextRenderer.h" #include "gui/CGUI.h" #include "gui/CGUIScrollBarVertical.h" #include "lib/timer.h" #include "lib/utf8.h" #include "ps/ConfigDB.h" #include "ps/GameSetup/Config.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "renderer/Renderer.h" #include extern int g_yres; const CStr CInput::EventNameTextEdit = "TextEdit"; const CStr CInput::EventNamePress = "Press"; const CStr CInput::EventNameTab = "Tab"; CInput::CInput(CGUI& pGUI) : IGUIObject(pGUI), IGUIScrollBarOwner(*static_cast(this)), m_iBufferPos(-1), m_iBufferPos_Tail(-1), m_SelectingText(), m_HorizontalScroll(), m_PrevTime(), m_CursorVisState(true), m_CursorBlinkRate(0.5), m_ComposingText(), m_GeneratedPlaceholderTextValid(false), m_iComposedLength(), m_iComposedPos(), m_iInsertPos(), m_BufferPosition(), m_BufferZone(), m_Caption(), m_CellID(), m_Font(), m_MaskChar(), m_Mask(), m_MaxLength(), m_MultiLine(), m_Readonly(), m_ScrollBar(), m_ScrollBarStyle(), m_Sprite(), m_SpriteSelectArea(), m_TextColor(), m_TextColorSelected() { RegisterSetting("buffer_position", m_BufferPosition); RegisterSetting("buffer_zone", m_BufferZone); RegisterSetting("caption", m_Caption); RegisterSetting("cell_id", m_CellID); RegisterSetting("font", m_Font); RegisterSetting("mask_char", m_MaskChar); RegisterSetting("mask", m_Mask); RegisterSetting("max_length", m_MaxLength); RegisterSetting("multiline", m_MultiLine); RegisterSetting("readonly", m_Readonly); RegisterSetting("scrollbar", m_ScrollBar); RegisterSetting("scrollbar_style", m_ScrollBarStyle); RegisterSetting("sprite", m_Sprite); RegisterSetting("sprite_selectarea", m_SpriteSelectArea); RegisterSetting("textcolor", m_TextColor); RegisterSetting("textcolor_selected", m_TextColorSelected); RegisterSetting("placeholder_text", m_PlaceholderText); RegisterSetting("placeholder_color", m_PlaceholderColor); CFG_GET_VAL("gui.cursorblinkrate", m_CursorBlinkRate); CGUIScrollBarVertical* bar = new CGUIScrollBarVertical(pGUI); bar->SetRightAligned(true); AddScrollBar(bar); } CInput::~CInput() { } void CInput::UpdateBufferPositionSetting() { SetSetting("buffer_position", m_iBufferPos, false); } void CInput::ClearComposedText() { m_Caption.erase(m_iInsertPos, m_iComposedLength); m_iBufferPos = m_iInsertPos; UpdateBufferPositionSetting(); m_iComposedLength = 0; m_iComposedPos = 0; } InReaction CInput::ManuallyHandleKeys(const SDL_Event_* ev) { ENSURE(m_iBufferPos != -1); switch (ev->ev.type) { case SDL_HOTKEYDOWN: { if (m_ComposingText) return IN_HANDLED; return ManuallyHandleHotkeyEvent(ev); } // SDL2 has a new method of text input that better supports Unicode and CJK // see https://wiki.libsdl.org/Tutorials/TextInput case SDL_TEXTINPUT: { if (m_Readonly) return IN_PASS; // Text has been committed, either single key presses or through an IME std::wstring text = wstring_from_utf8(ev->ev.text.text); // Check max length if (m_MaxLength != 0 && m_Caption.length() + text.length() > static_cast(m_MaxLength)) return IN_HANDLED; m_WantedX = 0.0f; if (SelectingText()) DeleteCurSelection(); if (m_ComposingText) { ClearComposedText(); m_ComposingText = false; } if (m_iBufferPos == static_cast(m_Caption.length())) m_Caption.append(text); else m_Caption.insert(m_iBufferPos, text); UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos+1); m_iBufferPos += text.length(); UpdateBufferPositionSetting(); m_iBufferPos_Tail = -1; UpdateAutoScroll(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); return IN_HANDLED; } case SDL_TEXTEDITING: { if (m_Readonly) return IN_PASS; // Text is being composed with an IME // TODO: indicate this by e.g. underlining the uncommitted text const char* rawText = ev->ev.edit.text; int rawLength = strlen(rawText); std::wstring wtext = wstring_from_utf8(rawText); m_WantedX = 0.0f; if (SelectingText()) DeleteCurSelection(); // Remember cursor position when text composition begins if (!m_ComposingText) m_iInsertPos = m_iBufferPos; else { // Composed text is replaced each time ClearComposedText(); } m_ComposingText = ev->ev.edit.start != 0 || rawLength != 0; if (m_ComposingText) { m_Caption.insert(m_iInsertPos, wtext); // The text buffer is limited to SDL_TEXTEDITINGEVENT_TEXT_SIZE bytes, yet start // increases without limit, so don't let it advance beyond the composed text length m_iComposedLength = wtext.length(); m_iComposedPos = ev->ev.edit.start < m_iComposedLength ? ev->ev.edit.start : m_iComposedLength; m_iBufferPos = m_iInsertPos + m_iComposedPos; // TODO: composed text selection - what does ev.edit.length do? m_iBufferPos_Tail = -1; } UpdateBufferPositionSetting(); UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos+1); UpdateAutoScroll(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); return IN_HANDLED; } case SDL_KEYDOWN: { // Since the GUI framework doesn't handle to set settings // in Unicode (CStrW), we'll simply retrieve the actual // pointer and edit that. SDL_Keycode keyCode = ev->ev.key.keysym.sym; // Regular text input is handled in SDL_TEXTINPUT, however keydown events are still sent (and they come first). // To make things work correctly, we pass through 'escape' which is a non-character key. // TODO: there are probably other keys that we could ignore, but recognizing "non-glyph" keys isn't that trivial. // Further, don't input text if modifiers other than shift are pressed (the user is presumably trying to perform a hotkey). if (keyCode == SDLK_ESCAPE || - g_keys[SDLK_LCTRL] || g_keys[SDLK_RCTRL] || - g_keys[SDLK_LALT] || g_keys[SDLK_RALT] || - g_keys[SDLK_LGUI] || g_keys[SDLK_RGUI]) + g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_LCTRL] || + g_scancodes[SDL_SCANCODE_LALT] || g_scancodes[SDL_SCANCODE_RALT] || + g_scancodes[SDL_SCANCODE_LGUI] || g_scancodes[SDL_SCANCODE_RGUI]) return IN_PASS; if (m_ComposingText) return IN_HANDLED; ManuallyImmutableHandleKeyDownEvent(keyCode); ManuallyMutableHandleKeyDownEvent(keyCode); UpdateBufferPositionSetting(); return IN_HANDLED; } default: { return IN_PASS; } } } void CInput::ManuallyMutableHandleKeyDownEvent(const SDL_Keycode keyCode) { if (m_Readonly) return; wchar_t cooked = 0; switch (keyCode) { case SDLK_TAB: { SendEvent(GUIM_TAB, EventNameTab); // Don't send a textedit event, because it should only // be sent if the GUI control changes the text break; } case SDLK_BACKSPACE: { m_WantedX = 0.0f; if (SelectingText()) DeleteCurSelection(); else { m_iBufferPos_Tail = -1; if (m_Caption.empty() || m_iBufferPos == 0) break; if (m_iBufferPos == static_cast(m_Caption.length())) m_Caption = m_Caption.Left(static_cast(m_Caption.length()) - 1); else m_Caption = m_Caption.Left(m_iBufferPos - 1) + m_Caption.Right(static_cast(m_Caption.length()) - m_iBufferPos); --m_iBufferPos; UpdateText(m_iBufferPos, m_iBufferPos + 1, m_iBufferPos); } UpdateAutoScroll(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); break; } case SDLK_DELETE: { m_WantedX = 0.0f; if (SelectingText()) DeleteCurSelection(); else { if (m_Caption.empty() || m_iBufferPos == static_cast(m_Caption.length())) break; m_Caption = m_Caption.Left(m_iBufferPos) + m_Caption.Right(static_cast(m_Caption.length()) - (m_iBufferPos + 1)); UpdateText(m_iBufferPos, m_iBufferPos + 1, m_iBufferPos); } UpdateAutoScroll(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); break; } case SDLK_KP_ENTER: case SDLK_RETURN: { // 'Return' should do a Press event for single liners (e.g. submitting forms) // otherwise a '\n' character will be added. if (!m_MultiLine) { SendEvent(GUIM_PRESSED, EventNamePress); break; } cooked = '\n'; // Change to '\n' and do default: FALLTHROUGH; } default: // Insert a character { // Regular input is handled via SDL_TEXTINPUT, so we should ignore it here. if (cooked == 0) return; // Check max length if (m_MaxLength != 0 && m_Caption.length() >= static_cast(m_MaxLength)) break; m_WantedX = 0.0f; if (SelectingText()) DeleteCurSelection(); m_iBufferPos_Tail = -1; if (m_iBufferPos == static_cast(m_Caption.length())) m_Caption += cooked; else m_Caption = m_Caption.Left(m_iBufferPos) + cooked + m_Caption.Right(static_cast(m_Caption.length()) - m_iBufferPos); UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos + 1); ++m_iBufferPos; UpdateAutoScroll(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); break; } } } void CInput::ManuallyImmutableHandleKeyDownEvent(const SDL_Keycode keyCode) { - bool shiftKeyPressed = g_keys[SDLK_RSHIFT] || g_keys[SDLK_LSHIFT]; + bool shiftKeyPressed = g_scancodes[SDL_SCANCODE_LSHIFT] || g_scancodes[SDL_SCANCODE_RSHIFT]; switch (keyCode) { case SDLK_HOME: { // If there's not a selection, we should create one now if (!shiftKeyPressed) { // Make sure a selection isn't created. m_iBufferPos_Tail = -1; } else if (!SelectingText()) { // Place tail at the current point: m_iBufferPos_Tail = m_iBufferPos; } m_iBufferPos = 0; m_WantedX = 0.0f; UpdateAutoScroll(); break; } case SDLK_END: { // If there's not a selection, we should create one now if (!shiftKeyPressed) { // Make sure a selection isn't created. m_iBufferPos_Tail = -1; } else if (!SelectingText()) { // Place tail at the current point: m_iBufferPos_Tail = m_iBufferPos; } m_iBufferPos = static_cast(m_Caption.length()); m_WantedX = 0.0f; UpdateAutoScroll(); break; } /** * Conventions for Left/Right when text is selected: * * References: * * Visual Studio * Visual Studio has the 'newer' approach, used by newer versions of * things, and in newer applications. A left press will always place * the pointer on the left edge of the selection, and then of course * remove the selection. Right will do the exact same thing. * If you have the pointer on the right edge and press right, it will * in other words just remove the selection. * * Windows (eg. Notepad) * A left press always takes the pointer a step to the left and * removes the selection as if it were never there in the first place. * Right of course does the same thing but to the right. * * I chose the Visual Studio convention. Used also in Word, gtk 2.0, MSN * Messenger. */ case SDLK_LEFT: { m_WantedX = 0.f; if (shiftKeyPressed || !SelectingText()) { if (!shiftKeyPressed) m_iBufferPos_Tail = -1; else if (!SelectingText()) m_iBufferPos_Tail = m_iBufferPos; if (m_iBufferPos > 0) --m_iBufferPos; } else { if (m_iBufferPos_Tail < m_iBufferPos) m_iBufferPos = m_iBufferPos_Tail; m_iBufferPos_Tail = -1; } UpdateAutoScroll(); break; } case SDLK_RIGHT: { m_WantedX = 0.0f; if (shiftKeyPressed || !SelectingText()) { if (!shiftKeyPressed) m_iBufferPos_Tail = -1; else if (!SelectingText()) m_iBufferPos_Tail = m_iBufferPos; if (m_iBufferPos < static_cast(m_Caption.length())) ++m_iBufferPos; } else { if (m_iBufferPos_Tail > m_iBufferPos) m_iBufferPos = m_iBufferPos_Tail; m_iBufferPos_Tail = -1; } UpdateAutoScroll(); break; } /** * Conventions for Up/Down when text is selected: * * References: * * Visual Studio * Visual Studio has a very strange approach, down takes you below the * selection to the next row, and up to the one prior to the whole * selection. The weird part is that it is always aligned as the * 'pointer'. I decided this is to much work for something that is * a bit arbitrary * * Windows (eg. Notepad) * Just like with left/right, the selection is destroyed and it moves * just as if there never were a selection. * * I chose the Notepad convention even though I use the VS convention with * left/right. */ case SDLK_UP: { if (!shiftKeyPressed) m_iBufferPos_Tail = -1; else if (!SelectingText()) m_iBufferPos_Tail = m_iBufferPos; std::list::iterator current = m_CharacterPositions.begin(); while (current != m_CharacterPositions.end()) { if (m_iBufferPos >= current->m_ListStart && m_iBufferPos <= current->m_ListStart + (int)current->m_ListOfX.size()) break; ++current; } float pos_x; if (m_iBufferPos - current->m_ListStart == 0) pos_x = 0.f; else pos_x = current->m_ListOfX[m_iBufferPos - current->m_ListStart - 1]; if (m_WantedX > pos_x) pos_x = m_WantedX; // Now change row: if (current != m_CharacterPositions.begin()) { --current; // Find X-position: m_iBufferPos = current->m_ListStart + GetXTextPosition(current, pos_x, m_WantedX); } // else we can't move up UpdateAutoScroll(); break; } case SDLK_DOWN: { if (!shiftKeyPressed) m_iBufferPos_Tail = -1; else if (!SelectingText()) m_iBufferPos_Tail = m_iBufferPos; std::list::iterator current = m_CharacterPositions.begin(); while (current != m_CharacterPositions.end()) { if (m_iBufferPos >= current->m_ListStart && m_iBufferPos <= current->m_ListStart + (int)current->m_ListOfX.size()) break; ++current; } float pos_x; if (m_iBufferPos - current->m_ListStart == 0) pos_x = 0.f; else pos_x = current->m_ListOfX[m_iBufferPos - current->m_ListStart - 1]; if (m_WantedX > pos_x) pos_x = m_WantedX; // Now change row: // Add first, so we can check if it's .end() ++current; if (current != m_CharacterPositions.end()) { // Find X-position: m_iBufferPos = current->m_ListStart + GetXTextPosition(current, pos_x, m_WantedX); } // else we can't move up UpdateAutoScroll(); break; } case SDLK_PAGEUP: { GetScrollBar(0).ScrollMinusPlenty(); UpdateAutoScroll(); break; } case SDLK_PAGEDOWN: { GetScrollBar(0).ScrollPlusPlenty(); UpdateAutoScroll(); break; } default: { break; } } } void CInput::SetupGeneratedPlaceholderText() { m_GeneratedPlaceholderText = CGUIText(m_pGUI, m_PlaceholderText, m_Font, 0, m_BufferZone, this); m_GeneratedPlaceholderTextValid = true; } InReaction CInput::ManuallyHandleHotkeyEvent(const SDL_Event_* ev) { - bool shiftKeyPressed = g_keys[SDLK_RSHIFT] || g_keys[SDLK_LSHIFT]; + bool shiftKeyPressed = g_scancodes[SDL_SCANCODE_LSHIFT] || g_scancodes[SDL_SCANCODE_RSHIFT]; std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "paste") { if (m_Readonly) return IN_PASS; m_WantedX = 0.0f; char* utf8_text = SDL_GetClipboardText(); if (!utf8_text) return IN_HANDLED; std::wstring text = wstring_from_utf8(utf8_text); SDL_free(utf8_text); // Check max length if (m_MaxLength != 0 && m_Caption.length() + text.length() > static_cast(m_MaxLength)) text = text.substr(0, static_cast(m_MaxLength) - m_Caption.length()); if (SelectingText()) DeleteCurSelection(); if (m_iBufferPos == static_cast(m_Caption.length())) m_Caption += text; else m_Caption = m_Caption.Left(m_iBufferPos) + text + m_Caption.Right(static_cast(m_Caption.length()) - m_iBufferPos); UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos+1); m_iBufferPos += static_cast(text.size()); UpdateAutoScroll(); UpdateBufferPositionSetting(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); return IN_HANDLED; } else if (hotkey == "copy" || hotkey == "cut") { if (m_Readonly && hotkey == "cut") return IN_PASS; m_WantedX = 0.0f; if (SelectingText()) { int virtualFrom; int virtualTo; if (m_iBufferPos_Tail >= m_iBufferPos) { virtualFrom = m_iBufferPos; virtualTo = m_iBufferPos_Tail; } else { virtualFrom = m_iBufferPos_Tail; virtualTo = m_iBufferPos; } CStrW text = m_Caption.Left(virtualTo).Right(virtualTo - virtualFrom); SDL_SetClipboardText(text.ToUTF8().c_str()); if (hotkey == "cut") { DeleteCurSelection(); UpdateAutoScroll(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); } } return IN_HANDLED; } else if (hotkey == "text.delete.left") { if (m_Readonly) return IN_PASS; m_WantedX = 0.0f; if (SelectingText()) DeleteCurSelection(); if (!m_Caption.empty() && m_iBufferPos != 0) { m_iBufferPos_Tail = m_iBufferPos; CStrW searchString = m_Caption.Left(m_iBufferPos); // If we are starting in whitespace, adjust position until we get a non whitespace while (m_iBufferPos > 0) { if (!iswspace(searchString[m_iBufferPos - 1])) break; m_iBufferPos--; } // If we end up on a punctuation char we just delete it (treat punct like a word) if (iswpunct(searchString[m_iBufferPos - 1])) m_iBufferPos--; else { // Now we are on a non white space character, adjust position to char after next whitespace char is found while (m_iBufferPos > 0) { if (iswspace(searchString[m_iBufferPos - 1]) || iswpunct(searchString[m_iBufferPos - 1])) break; m_iBufferPos--; } } UpdateBufferPositionSetting(); DeleteCurSelection(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); } UpdateAutoScroll(); return IN_HANDLED; } else if (hotkey == "text.delete.right") { if (m_Readonly) return IN_PASS; m_WantedX = 0.0f; if (SelectingText()) DeleteCurSelection(); if (!m_Caption.empty() && m_iBufferPos < static_cast(m_Caption.length())) { // Delete the word to the right of the cursor m_iBufferPos_Tail = m_iBufferPos; // Delete chars to the right unit we hit whitespace while (++m_iBufferPos < static_cast(m_Caption.length())) { if (iswspace(m_Caption[m_iBufferPos]) || iswpunct(m_Caption[m_iBufferPos])) break; } // Eliminate any whitespace behind the word we just deleted while (m_iBufferPos < static_cast(m_Caption.length())) { if (!iswspace(m_Caption[m_iBufferPos])) break; ++m_iBufferPos; } UpdateBufferPositionSetting(); DeleteCurSelection(); } UpdateAutoScroll(); SendEvent(GUIM_TEXTEDIT, EventNameTextEdit); return IN_HANDLED; } else if (hotkey == "text.move.left") { m_WantedX = 0.0f; if (shiftKeyPressed || !SelectingText()) { if (!shiftKeyPressed) m_iBufferPos_Tail = -1; else if (!SelectingText()) m_iBufferPos_Tail = m_iBufferPos; if (!m_Caption.empty() && m_iBufferPos != 0) { CStrW searchString = m_Caption.Left(m_iBufferPos); // If we are starting in whitespace, adjust position until we get a non whitespace while (m_iBufferPos > 0) { if (!iswspace(searchString[m_iBufferPos - 1])) break; m_iBufferPos--; } // If we end up on a puctuation char we just select it (treat punct like a word) if (iswpunct(searchString[m_iBufferPos - 1])) m_iBufferPos--; else { // Now we are on a non white space character, adjust position to char after next whitespace char is found while (m_iBufferPos > 0) { if (iswspace(searchString[m_iBufferPos - 1]) || iswpunct(searchString[m_iBufferPos - 1])) break; m_iBufferPos--; } } } } else { if (m_iBufferPos_Tail < m_iBufferPos) m_iBufferPos = m_iBufferPos_Tail; m_iBufferPos_Tail = -1; } UpdateBufferPositionSetting(); UpdateAutoScroll(); return IN_HANDLED; } else if (hotkey == "text.move.right") { m_WantedX = 0.0f; if (shiftKeyPressed || !SelectingText()) { if (!shiftKeyPressed) m_iBufferPos_Tail = -1; else if (!SelectingText()) m_iBufferPos_Tail = m_iBufferPos; if (!m_Caption.empty() && m_iBufferPos < static_cast(m_Caption.length())) { // Select chars to the right until we hit whitespace while (++m_iBufferPos < static_cast(m_Caption.length())) { if (iswspace(m_Caption[m_iBufferPos]) || iswpunct(m_Caption[m_iBufferPos])) break; } // Also select any whitespace following the word we just selected while (m_iBufferPos < static_cast(m_Caption.length())) { if (!iswspace(m_Caption[m_iBufferPos])) break; ++m_iBufferPos; } } } else { if (m_iBufferPos_Tail > m_iBufferPos) m_iBufferPos = m_iBufferPos_Tail; m_iBufferPos_Tail = -1; } UpdateBufferPositionSetting(); UpdateAutoScroll(); return IN_HANDLED; } return IN_PASS; } void CInput::ResetStates() { IGUIObject::ResetStates(); IGUIScrollBarOwner::ResetStates(); } void CInput::HandleMessage(SGUIMessage& Message) { IGUIObject::HandleMessage(Message); IGUIScrollBarOwner::HandleMessage(Message); switch (Message.type) { case GUIM_SETTINGS_UPDATED: { // Update scroll-bar // TODO Gee: (2004-09-01) Is this really updated each time it should? if (m_ScrollBar && (Message.value == "size" || Message.value == "z" || Message.value == "absolute")) { GetScrollBar(0).SetX(m_CachedActualSize.right); GetScrollBar(0).SetY(m_CachedActualSize.top); GetScrollBar(0).SetZ(GetBufferedZ()); GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top); } // Update scrollbar if (Message.value == "scrollbar_style") GetScrollBar(0).SetScrollBarStyle(m_ScrollBarStyle); if (Message.value == "buffer_position") { m_iBufferPos = m_MaxLength != 0 ? std::min(m_MaxLength, m_BufferPosition) : m_BufferPosition; m_iBufferPos_Tail = -1; // position change resets selection } if (Message.value == "size" || Message.value == "z" || Message.value == "font" || Message.value == "absolute" || Message.value == "caption" || Message.value == "scrollbar" || Message.value == "scrollbar_style") { UpdateText(); } if (Message.value == "multiline") { if (!m_MultiLine) GetScrollBar(0).SetLength(0.f); else GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top); UpdateText(); } if (Message.value == "placeholder_text" || Message.value == "size" || Message.value == "font" || Message.value == "z" || Message.value == "text_valign") { m_GeneratedPlaceholderTextValid = false; } UpdateAutoScroll(); break; } case GUIM_MOUSE_PRESS_LEFT: { // Check if we're selecting the scrollbar if (m_ScrollBar && m_MultiLine && GetScrollBar(0).GetStyle()) { if (m_pGUI.GetMousePos().x > m_CachedActualSize.right - GetScrollBar(0).GetStyle()->m_Width) break; } if (m_ComposingText) break; // Okay, this section is about pressing the mouse and // choosing where the point should be placed. For // instance, if we press between a and b, the point // should of course be placed accordingly. Other // special cases are handled like the input box norms. - if (g_keys[SDLK_RSHIFT] || g_keys[SDLK_LSHIFT]) + if (g_scancodes[SDL_SCANCODE_LSHIFT] || g_scancodes[SDL_SCANCODE_RSHIFT]) m_iBufferPos = GetMouseHoveringTextPosition(); else m_iBufferPos = m_iBufferPos_Tail = GetMouseHoveringTextPosition(); m_SelectingText = true; UpdateAutoScroll(); // If we immediately release the button it will just be seen as a click // for the user though. break; } case GUIM_MOUSE_DBLCLICK_LEFT: { if (m_ComposingText) break; if (m_Caption.empty()) break; m_iBufferPos = m_iBufferPos_Tail = GetMouseHoveringTextPosition(); if (m_iBufferPos >= (int)m_Caption.length()) m_iBufferPos = m_iBufferPos_Tail = m_Caption.length() - 1; // See if we are clicking over whitespace if (iswspace(m_Caption[m_iBufferPos])) { // see if we are in a section of whitespace greater than one character if ((m_iBufferPos + 1 < (int) m_Caption.length() && iswspace(m_Caption[m_iBufferPos + 1])) || (m_iBufferPos - 1 > 0 && iswspace(m_Caption[m_iBufferPos - 1]))) { // // We are clicking in an area with more than one whitespace character // so we select both the word to the left and then the word to the right // // [1] First the left // skip the whitespace while (m_iBufferPos > 0) { if (!iswspace(m_Caption[m_iBufferPos - 1])) break; m_iBufferPos--; } // now go until we hit white space or punctuation while (m_iBufferPos > 0) { if (iswspace(m_Caption[m_iBufferPos - 1])) break; m_iBufferPos--; if (iswpunct(m_Caption[m_iBufferPos])) break; } // [2] Then the right // go right until we are not in whitespace while (++m_iBufferPos_Tail < static_cast(m_Caption.length())) { if (!iswspace(m_Caption[m_iBufferPos_Tail])) break; } if (m_iBufferPos_Tail == static_cast(m_Caption.length())) break; // now go to the right until we hit whitespace or punctuation while (++m_iBufferPos_Tail < static_cast(m_Caption.length())) { if (iswspace(m_Caption[m_iBufferPos_Tail]) || iswpunct(m_Caption[m_iBufferPos_Tail])) break; } } else { // single whitespace so select word to the right while (++m_iBufferPos_Tail < static_cast(m_Caption.length())) { if (!iswspace(m_Caption[m_iBufferPos_Tail])) break; } if (m_iBufferPos_Tail == static_cast(m_Caption.length())) break; // Don't include the leading whitespace m_iBufferPos = m_iBufferPos_Tail; // now go to the right until we hit whitespace or punctuation while (++m_iBufferPos_Tail < static_cast(m_Caption.length())) { if (iswspace(m_Caption[m_iBufferPos_Tail]) || iswpunct(m_Caption[m_iBufferPos_Tail])) break; } } } else { // clicked on non-whitespace so select current word // go until we hit white space or punctuation while (m_iBufferPos > 0) { if (iswspace(m_Caption[m_iBufferPos - 1])) break; m_iBufferPos--; if (iswpunct(m_Caption[m_iBufferPos])) break; } // go to the right until we hit whitespace or punctuation while (++m_iBufferPos_Tail < static_cast(m_Caption.length())) if (iswspace(m_Caption[m_iBufferPos_Tail]) || iswpunct(m_Caption[m_iBufferPos_Tail])) break; } UpdateAutoScroll(); break; } case GUIM_MOUSE_RELEASE_LEFT: { if (m_SelectingText) m_SelectingText = false; break; } case GUIM_MOUSE_MOTION: { // If we just pressed down and started to move before releasing // this is one way of selecting larger portions of text. if (m_SelectingText) { // Actually, first we need to re-check that the mouse button is // really pressed (it can be released while outside the control. if (!g_mouse_buttons[SDL_BUTTON_LEFT]) m_SelectingText = false; else m_iBufferPos = GetMouseHoveringTextPosition(); UpdateAutoScroll(); } break; } case GUIM_LOAD: { GetScrollBar(0).SetX(m_CachedActualSize.right); GetScrollBar(0).SetY(m_CachedActualSize.top); GetScrollBar(0).SetZ(GetBufferedZ()); GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top); GetScrollBar(0).SetScrollBarStyle(m_ScrollBarStyle); UpdateText(); UpdateAutoScroll(); break; } case GUIM_GOT_FOCUS: { m_iBufferPos = 0; m_PrevTime = 0.0; m_CursorVisState = false; // Tell the IME where to draw the candidate list SDL_Rect rect; rect.h = m_CachedActualSize.GetSize().cy; rect.w = m_CachedActualSize.GetSize().cx; rect.x = m_CachedActualSize.TopLeft().x; rect.y = m_CachedActualSize.TopLeft().y; SDL_SetTextInputRect(&rect); SDL_StartTextInput(); break; } case GUIM_LOST_FOCUS: { if (m_ComposingText) { // Simulate a final text editing event to clear the composition SDL_Event_ evt; evt.ev.type = SDL_TEXTEDITING; evt.ev.edit.length = 0; evt.ev.edit.start = 0; evt.ev.edit.text[0] = 0; ManuallyHandleKeys(&evt); } SDL_StopTextInput(); m_iBufferPos = -1; m_iBufferPos_Tail = -1; break; } default: { break; } } UpdateBufferPositionSetting(); } void CInput::UpdateCachedSize() { // If an ancestor's size changed, this will let us intercept the change and // update our scrollbar positions IGUIObject::UpdateCachedSize(); if (m_ScrollBar) { GetScrollBar(0).SetX(m_CachedActualSize.right); GetScrollBar(0).SetY(m_CachedActualSize.top); GetScrollBar(0).SetZ(GetBufferedZ()); GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top); } m_GeneratedPlaceholderTextValid = false; } void CInput::Draw() { float bz = GetBufferedZ(); if (m_CursorBlinkRate > 0.0) { // check if the cursor visibility state needs to be changed double currTime = timer_Time(); if (currTime - m_PrevTime >= m_CursorBlinkRate) { m_CursorVisState = !m_CursorVisState; m_PrevTime = currTime; } } else // should always be visible m_CursorVisState = true; // First call draw on ScrollBarOwner if (m_ScrollBar && m_MultiLine) IGUIScrollBarOwner::Draw(); CStrIntern font_name(m_Font.ToUTF8()); wchar_t mask_char = L'*'; if (m_Mask && m_MaskChar.length() > 0) mask_char = m_MaskChar[0]; m_pGUI.DrawSprite(m_Sprite, m_CellID, bz, m_CachedActualSize); float scroll = 0.f; if (m_ScrollBar && m_MultiLine) scroll = GetScrollBar(0).GetPos(); CFontMetrics font(font_name); // We'll have to setup clipping manually, since we're doing the rendering manually. CRect cliparea(m_CachedActualSize); // First we'll figure out the clipping area, which is the cached actual size // substracted by an optional scrollbar if (m_ScrollBar) { scroll = GetScrollBar(0).GetPos(); // substract scrollbar from cliparea if (cliparea.right > GetScrollBar(0).GetOuterRect().left && cliparea.right <= GetScrollBar(0).GetOuterRect().right) cliparea.right = GetScrollBar(0).GetOuterRect().left; if (cliparea.left >= GetScrollBar(0).GetOuterRect().left && cliparea.left < GetScrollBar(0).GetOuterRect().right) cliparea.left = GetScrollBar(0).GetOuterRect().right; } if (cliparea != CRect()) { glEnable(GL_SCISSOR_TEST); glScissor( cliparea.left * g_GuiScale, g_yres - cliparea.bottom * g_GuiScale, cliparea.GetWidth() * g_GuiScale, cliparea.GetHeight() * g_GuiScale); } // These are useful later. int VirtualFrom, VirtualTo; if (m_iBufferPos_Tail >= m_iBufferPos) { VirtualFrom = m_iBufferPos; VirtualTo = m_iBufferPos_Tail; } else { VirtualFrom = m_iBufferPos_Tail; VirtualTo = m_iBufferPos; } // Get the height of this font. float h = (float)font.GetHeight(); float ls = (float)font.GetLineSpacing(); CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_gui_text); CTextRenderer textRenderer(tech->GetShader()); textRenderer.Font(font_name); // Set the Z to somewhat more, so we can draw a selected area between the // the control and the text. textRenderer.Translate( (float)(int)(m_CachedActualSize.left) + m_BufferZone, (float)(int)(m_CachedActualSize.top+h) + m_BufferZone, bz+0.1f); // U+FE33: PRESENTATION FORM FOR VERTICAL LOW LINE // (sort of like a | which is aligned to the left of most characters) float buffered_y = -scroll + m_BufferZone; // When selecting larger areas, we need to draw a rectangle box // around it, and this is to keep track of where the box // started, because we need to follow the iteration until we // reach the end, before we can actually draw it. bool drawing_box = false; float box_x = 0.f; float x_pointer = 0.f; // If we have a selecting box (i.e. when you have selected letters, not just when // the pointer is between two letters) we need to process all letters once // before we do it the second time and render all the text. We can't do it // in the same loop because text will have been drawn, so it will disappear when // drawn behind the text that has already been drawn. Confusing, well it's necessary // (I think). if (SelectingText()) { // Now m_iBufferPos_Tail can be of both sides of m_iBufferPos, // just like you can select from right to left, as you can // left to right. Is there a difference? Yes, the pointer // be placed accordingly, so that if you select shift and // expand this selection, it will expand on appropriate side. // Anyway, since the drawing procedure needs "To" to be // greater than from, we need virtual values that might switch // place. int virtualFrom = 0; int virtualTo = 0; if (m_iBufferPos_Tail >= m_iBufferPos) { virtualFrom = m_iBufferPos; virtualTo = m_iBufferPos_Tail; } else { virtualFrom = m_iBufferPos_Tail; virtualTo = m_iBufferPos; } bool done = false; for (std::list::const_iterator it = m_CharacterPositions.begin(); it != m_CharacterPositions.end(); ++it, buffered_y += ls, x_pointer = 0.f) { if (m_MultiLine && buffered_y > m_CachedActualSize.GetHeight()) break; // We might as well use 'i' here to iterate, because we need it // (often compared against ints, so don't make it size_t) for (int i = 0; i < (int)it->m_ListOfX.size()+2; ++i) { if (it->m_ListStart + i == virtualFrom) { // we won't actually draw it now, because we don't // know the width of each glyph to that position. // we need to go along with the iteration, and // make a mark where the box started: drawing_box = true; // will turn false when finally rendered. // Get current x position box_x = x_pointer; } const bool at_end = (i == (int)it->m_ListOfX.size()+1); if (drawing_box && (it->m_ListStart + i == virtualTo || at_end)) { // Depending on if it's just a row change, or if it's // the end of the select box, do slightly different things. if (at_end) { if (it->m_ListStart + i != virtualFrom) // and actually add a white space! yes, this is done in any common input x_pointer += font.GetCharacterWidth(L' '); } else { drawing_box = false; done = true; } CRect rect; // Set 'rect' depending on if it's a multiline control, or a one-line control if (m_MultiLine) { rect = CRect( m_CachedActualSize.left + box_x + m_BufferZone, m_CachedActualSize.top + buffered_y + (h - ls) / 2, m_CachedActualSize.left + x_pointer + m_BufferZone, m_CachedActualSize.top + buffered_y + (h + ls) / 2); if (rect.bottom < m_CachedActualSize.top) continue; if (rect.top < m_CachedActualSize.top) rect.top = m_CachedActualSize.top; if (rect.bottom > m_CachedActualSize.bottom) rect.bottom = m_CachedActualSize.bottom; } else // if one-line { rect = CRect( m_CachedActualSize.left + box_x + m_BufferZone - m_HorizontalScroll, m_CachedActualSize.top + buffered_y + (h - ls) / 2, m_CachedActualSize.left + x_pointer + m_BufferZone - m_HorizontalScroll, m_CachedActualSize.top + buffered_y + (h + ls) / 2); if (rect.left < m_CachedActualSize.left) rect.left = m_CachedActualSize.left; if (rect.right > m_CachedActualSize.right) rect.right = m_CachedActualSize.right; } m_pGUI.DrawSprite(m_SpriteSelectArea, m_CellID, bz + 0.05f, rect); } if (i < (int)it->m_ListOfX.size()) { if (!m_Mask) x_pointer += font.GetCharacterWidth(m_Caption[it->m_ListStart + i]); else x_pointer += font.GetCharacterWidth(mask_char); } } if (done) break; // If we're about to draw a box, and all of a sudden changes // line, we need to draw that line's box, and then reset // the box drawing to the beginning of the new line. if (drawing_box) box_x = 0.f; } } // Reset some from previous run buffered_y = -scroll; // Setup initial color (then it might change and change back, when drawing selected area) textRenderer.Color(m_TextColor); tech->BeginPass(); bool using_selected_color = false; for (std::list::const_iterator it = m_CharacterPositions.begin(); it != m_CharacterPositions.end(); ++it, buffered_y += ls) { if (buffered_y + m_BufferZone >= -ls || !m_MultiLine) { if (m_MultiLine && buffered_y + m_BufferZone > m_CachedActualSize.GetHeight()) break; CMatrix3D savedTransform = textRenderer.GetTransform(); // Text must always be drawn in integer values. So we have to convert scroll if (m_MultiLine) textRenderer.Translate(0.f, -(float)(int)scroll, 0.f); else textRenderer.Translate(-(float)(int)m_HorizontalScroll, 0.f, 0.f); // We might as well use 'i' here, because we need it // (often compared against ints, so don't make it size_t) for (int i = 0; i < (int)it->m_ListOfX.size()+1; ++i) { if (!m_MultiLine && i < (int)it->m_ListOfX.size()) { if (it->m_ListOfX[i] - m_HorizontalScroll < -m_BufferZone) { // We still need to translate the OpenGL matrix if (i == 0) textRenderer.Translate(it->m_ListOfX[i], 0.f, 0.f); else textRenderer.Translate(it->m_ListOfX[i] - it->m_ListOfX[i-1], 0.f, 0.f); continue; } } // End of selected area, change back color if (SelectingText() && it->m_ListStart + i == VirtualTo) { using_selected_color = false; textRenderer.Color(m_TextColor); } // selecting only one, then we need only to draw a cursor. if (i != (int)it->m_ListOfX.size() && it->m_ListStart + i == m_iBufferPos && m_CursorVisState) textRenderer.Put(0.0f, 0.0f, L"_"); // Drawing selected area if (SelectingText() && it->m_ListStart + i >= VirtualFrom && it->m_ListStart + i < VirtualTo && !using_selected_color) { using_selected_color = true; textRenderer.Color(m_TextColorSelected); } if (i != (int)it->m_ListOfX.size()) { if (!m_Mask) textRenderer.PrintfAdvance(L"%lc", m_Caption[it->m_ListStart + i]); else textRenderer.PrintfAdvance(L"%lc", mask_char); } // check it's now outside a one-liner, then we'll break if (!m_MultiLine && i < (int)it->m_ListOfX.size() && it->m_ListOfX[i] - m_HorizontalScroll > m_CachedActualSize.GetWidth() - m_BufferZone) break; } if (it->m_ListStart + (int)it->m_ListOfX.size() == m_iBufferPos) { textRenderer.Color(m_TextColor); if (m_CursorVisState) textRenderer.PutAdvance(L"_"); if (using_selected_color) textRenderer.Color(m_TextColorSelected); } textRenderer.SetTransform(savedTransform); } textRenderer.Translate(0.f, ls, 0.f); } textRenderer.Render(); if (cliparea != CRect()) glDisable(GL_SCISSOR_TEST); tech->EndPass(); if (m_Caption.empty() && !m_PlaceholderText.GetRawString().empty()) DrawPlaceholderText(bz, cliparea); } void CInput::DrawPlaceholderText(float z, const CRect& clipping) { if (!m_GeneratedPlaceholderTextValid) SetupGeneratedPlaceholderText(); m_GeneratedPlaceholderText.Draw(m_pGUI, m_PlaceholderColor, m_CachedActualSize.TopLeft(), z, clipping); } void CInput::UpdateText(int from, int to_before, int to_after) { if (m_MaxLength != 0 && m_Caption.length() > static_cast(m_MaxLength)) m_Caption = m_Caption.substr(0, m_MaxLength); CStrIntern font_name(m_Font.ToUTF8()); wchar_t mask_char = L'*'; if (m_Mask && m_MaskChar.length() > 0) mask_char = m_MaskChar[0]; // Ensure positions are valid after caption changes m_iBufferPos = std::min(m_iBufferPos, static_cast(m_Caption.size())); m_iBufferPos_Tail = std::min(m_iBufferPos_Tail, static_cast(m_Caption.size())); UpdateBufferPositionSetting(); if (font_name.empty()) { // Destroy everything stored, there's no font, so there can be no data. m_CharacterPositions.clear(); return; } SRow row; row.m_ListStart = 0; int to = 0; // make sure it's initialized if (to_before == -1) to = static_cast(m_Caption.length()); CFontMetrics font(font_name); std::list::iterator current_line; // Used to ... TODO int check_point_row_start = -1; int check_point_row_end = -1; // Reset if (from == 0 && to_before == -1) { m_CharacterPositions.clear(); current_line = m_CharacterPositions.begin(); } else { ENSURE(to_before != -1); std::list::iterator destroy_row_from; std::list::iterator destroy_row_to; // Used to check if the above has been set to anything, // previously a comparison like: // destroy_row_from == std::list::iterator() // ... was used, but it didn't work with GCC. bool destroy_row_from_used = false; bool destroy_row_to_used = false; // Iterate, and remove everything between 'from' and 'to_before' // actually remove the entire lines they are on, it'll all have // to be redone. And when going along, we'll delete a row at a time // when continuing to see how much more after 'to' we need to remake. int i = 0; for (std::list::iterator it = m_CharacterPositions.begin(); it != m_CharacterPositions.end(); ++it, ++i) { if (!destroy_row_from_used && it->m_ListStart > from) { // Destroy the previous line, and all to 'to_before' destroy_row_from = it; --destroy_row_from; destroy_row_from_used = true; // For the rare case that we might remove characters to a word // so that it suddenly fits on the previous row, // we need to by standards re-do the whole previous line too // (if one exists) if (destroy_row_from != m_CharacterPositions.begin()) --destroy_row_from; } if (!destroy_row_to_used && it->m_ListStart > to_before) { destroy_row_to = it; destroy_row_to_used = true; // If it isn't the last row, we'll add another row to delete, // just so we can see if the last restorted line is // identical to what it was before. If it isn't, then we'll // have to continue. // 'check_point_row_start' is where we store how the that // line looked. if (destroy_row_to != m_CharacterPositions.end()) { check_point_row_start = destroy_row_to->m_ListStart; check_point_row_end = check_point_row_start + (int)destroy_row_to->m_ListOfX.size(); if (destroy_row_to->m_ListOfX.empty()) ++check_point_row_end; } ++destroy_row_to; break; } } if (!destroy_row_from_used) { destroy_row_from = m_CharacterPositions.end(); --destroy_row_from; // As usual, let's destroy another row back if (destroy_row_from != m_CharacterPositions.begin()) --destroy_row_from; current_line = destroy_row_from; } if (!destroy_row_to_used) { destroy_row_to = m_CharacterPositions.end(); check_point_row_start = -1; } // set 'from' to the row we'll destroy from // and 'to' to the row we'll destroy to from = destroy_row_from->m_ListStart; if (destroy_row_to != m_CharacterPositions.end()) to = destroy_row_to->m_ListStart; // notice it will iterate [from, to), so it will never reach to. else to = static_cast(m_Caption.length()); // Setup the first row row.m_ListStart = destroy_row_from->m_ListStart; std::list::iterator temp_it = destroy_row_to; --temp_it; current_line = m_CharacterPositions.erase(destroy_row_from, destroy_row_to); // If there has been a change in number of characters // we need to change all m_ListStart that comes after // the interval we just destroyed. We'll change all // values with the delta change of the string length. int delta = to_after - to_before; if (delta != 0) { for (std::list::iterator it = current_line; it != m_CharacterPositions.end(); ++it) it->m_ListStart += delta; // Update our check point too! check_point_row_start += delta; check_point_row_end += delta; if (to != static_cast(m_Caption.length())) to += delta; } } int last_word_started = from; float x_pos = 0.f; //if (to_before != -1) // return; for (int i = from; i < to; ++i) { if (m_Caption[i] == L'\n' && m_MultiLine) { if (i == to-1 && to != static_cast(m_Caption.length())) break; // it will be added outside current_line = m_CharacterPositions.insert(current_line, row); ++current_line; // Setup the next row: row.m_ListOfX.clear(); row.m_ListStart = i+1; x_pos = 0.f; } else { if (m_Caption[i] == L' '/* || TODO Gee (2004-10-13): the '-' disappears, fix. m_Caption[i] == L'-'*/) last_word_started = i+1; if (!m_Mask) x_pos += font.GetCharacterWidth(m_Caption[i]); else x_pos += font.GetCharacterWidth(mask_char); if (x_pos >= GetTextAreaWidth() && m_MultiLine) { // The following decides whether it will word-wrap a word, // or if it's only one word on the line, where it has to // break the word apart. if (last_word_started == row.m_ListStart) { last_word_started = i; row.m_ListOfX.resize(row.m_ListOfX.size() - (i-last_word_started)); //row.m_ListOfX.push_back(x_pos); //continue; } else { // regular word-wrap row.m_ListOfX.resize(row.m_ListOfX.size() - (i-last_word_started+1)); } // Now, create a new line: // notice: when we enter a newline, you can stand with the cursor // both before and after that character, being on different // rows. With automatic word-wrapping, that is not possible. Which // is intuitively correct. current_line = m_CharacterPositions.insert(current_line, row); ++current_line; // Setup the next row: row.m_ListOfX.clear(); row.m_ListStart = last_word_started; i = last_word_started-1; x_pos = 0.f; } else // Get width of this character: row.m_ListOfX.push_back(x_pos); } // Check if it's the last iteration, and we're not revising the whole string // because in that case, more word-wrapping might be needed. // also check if the current line isn't the end if (to_before != -1 && i == to-1 && current_line != m_CharacterPositions.end()) { // check all rows and see if any existing if (row.m_ListStart != check_point_row_start) { std::list::iterator destroy_row_from; std::list::iterator destroy_row_to; // Are used to check if the above has been set to anything, // previously a comparison like: // destroy_row_from == std::list::iterator() // was used, but it didn't work with GCC. bool destroy_row_from_used = false; bool destroy_row_to_used = false; // Iterate, and remove everything between 'from' and 'to_before' // actually remove the entire lines they are on, it'll all have // to be redone. And when going along, we'll delete a row at a time // when continuing to see how much more after 'to' we need to remake. for (std::list::iterator it = m_CharacterPositions.begin(); it != m_CharacterPositions.end(); ++it) { if (!destroy_row_from_used && it->m_ListStart > check_point_row_start) { // Destroy the previous line, and all to 'to_before' //if (i >= 2) // destroy_row_from = it-2; //else // destroy_row_from = it-1; destroy_row_from = it; destroy_row_from_used = true; //--destroy_row_from; } if (!destroy_row_to_used && it->m_ListStart > check_point_row_end) { destroy_row_to = it; destroy_row_to_used = true; // If it isn't the last row, we'll add another row to delete, // just so we can see if the last restorted line is // identical to what it was before. If it isn't, then we'll // have to continue. // 'check_point_row_start' is where we store how the that // line looked. if (destroy_row_to != m_CharacterPositions.end()) { check_point_row_start = destroy_row_to->m_ListStart; check_point_row_end = check_point_row_start + (int)destroy_row_to->m_ListOfX.size(); if (destroy_row_to->m_ListOfX.empty()) ++check_point_row_end; } else check_point_row_start = check_point_row_end = -1; ++destroy_row_to; break; } } if (!destroy_row_from_used) { destroy_row_from = m_CharacterPositions.end(); --destroy_row_from; current_line = destroy_row_from; } if (!destroy_row_to_used) { destroy_row_to = m_CharacterPositions.end(); check_point_row_start = check_point_row_end = -1; } if (destroy_row_to != m_CharacterPositions.end()) to = destroy_row_to->m_ListStart; // notice it will iterate [from, to[, so it will never reach to. else to = static_cast(m_Caption.length()); // Set current line, new rows will be added before current_line, so // we'll choose the destroy_row_to, because it won't be deleted // in the coming erase. current_line = destroy_row_to; m_CharacterPositions.erase(destroy_row_from, destroy_row_to); } // else, the for loop will end naturally. } } // This is kind of special, when we renew a some lines, then the last // one will sometimes end with a space (' '), that really should // be omitted when word-wrapping. So we'll check if the last row // we'll add has got the same value as the next row. if (current_line != m_CharacterPositions.end()) { if (row.m_ListStart + (int)row.m_ListOfX.size() == current_line->m_ListStart) row.m_ListOfX.resize(row.m_ListOfX.size()-1); } // add the final row (even if empty) m_CharacterPositions.insert(current_line, row); if (m_ScrollBar) { GetScrollBar(0).SetScrollRange(m_CharacterPositions.size() * font.GetLineSpacing() + m_BufferZone * 2.f); GetScrollBar(0).SetScrollSpace(m_CachedActualSize.GetHeight()); } } int CInput::GetMouseHoveringTextPosition() const { if (m_CharacterPositions.empty()) return 0; // Return position int retPosition; std::list::const_iterator current = m_CharacterPositions.begin(); CPos mouse = m_pGUI.GetMousePos(); if (m_MultiLine) { float scroll = 0.f; if (m_ScrollBar) scroll = GetScrollBarPos(0); // Now get the height of the font. // TODO: Get the real font CFontMetrics font(CStrIntern(m_Font.ToUTF8())); float spacing = (float)font.GetLineSpacing(); // Change mouse position relative to text. mouse -= m_CachedActualSize.TopLeft(); mouse.x -= m_BufferZone; mouse.y += scroll - m_BufferZone; int row = (int)((mouse.y) / spacing); if (row < 0) row = 0; if (row > (int)m_CharacterPositions.size()-1) row = (int)m_CharacterPositions.size()-1; // TODO Gee (2004-11-21): Okay, I need a 'std::list' for some reasons, but I would really like to // be able to get the specific element here. This is hopefully a temporary hack. for (int i = 0; i < row; ++i) ++current; } else { // current is already set to begin, // but we'll change the mouse.x to fit our horizontal scrolling mouse -= m_CachedActualSize.TopLeft(); mouse.x -= m_BufferZone - m_HorizontalScroll; // mouse.y is moot } retPosition = current->m_ListStart; // Okay, now loop through the glyphs to find the appropriate X position float dummy; retPosition += GetXTextPosition(current, mouse.x, dummy); return retPosition; } // Does not process horizontal scrolling, 'x' must be modified before inputted. int CInput::GetXTextPosition(const std::list::const_iterator& current, const float& x, float& wanted) const { int ret = 0; float previous = 0.f; int i = 0; for (std::vector::const_iterator it = current->m_ListOfX.begin(); it != current->m_ListOfX.end(); ++it, ++i) { if (*it >= x) { if (x - previous >= *it - x) ret += i+1; else ret += i; break; } previous = *it; } // If a position wasn't found, we will assume the last // character of that line. if (i == (int)current->m_ListOfX.size()) { ret += i; wanted = x; } else wanted = 0.f; return ret; } void CInput::DeleteCurSelection() { int virtualFrom; int virtualTo; if (m_iBufferPos_Tail >= m_iBufferPos) { virtualFrom = m_iBufferPos; virtualTo = m_iBufferPos_Tail; } else { virtualFrom = m_iBufferPos_Tail; virtualTo = m_iBufferPos; } m_Caption = m_Caption.Left(virtualFrom) + m_Caption.Right(static_cast(m_Caption.length()) - virtualTo); UpdateText(virtualFrom, virtualTo, virtualFrom); // Remove selection m_iBufferPos_Tail = -1; m_iBufferPos = virtualFrom; UpdateBufferPositionSetting(); } bool CInput::SelectingText() const { return m_iBufferPos_Tail != -1 && m_iBufferPos_Tail != m_iBufferPos; } float CInput::GetTextAreaWidth() { if (m_ScrollBar && GetScrollBar(0).GetStyle()) return m_CachedActualSize.GetWidth() - m_BufferZone * 2.f - GetScrollBar(0).GetStyle()->m_Width; return m_CachedActualSize.GetWidth() - m_BufferZone * 2.f; } void CInput::UpdateAutoScroll() { // Autoscrolling up and down if (m_MultiLine) { if (!m_ScrollBar) return; const float scroll = GetScrollBar(0).GetPos(); // Now get the height of the font. // TODO: Get the real font CFontMetrics font(CStrIntern(m_Font.ToUTF8())); float spacing = (float)font.GetLineSpacing(); //float height = font.GetHeight(); // TODO Gee (2004-11-21): Okay, I need a 'std::list' for some reasons, but I would really like to // be able to get the specific element here. This is hopefully a temporary hack. std::list::iterator current = m_CharacterPositions.begin(); int row = 0; while (current != m_CharacterPositions.end()) { if (m_iBufferPos >= current->m_ListStart && m_iBufferPos <= current->m_ListStart + (int)current->m_ListOfX.size()) break; ++current; ++row; } // If scrolling down if (-scroll + static_cast(row + 1) * spacing + m_BufferZone * 2.f > m_CachedActualSize.GetHeight()) { // Scroll so the selected row is shown completely, also with m_BufferZone length to the edge. GetScrollBar(0).SetPos(static_cast(row + 1) * spacing - m_CachedActualSize.GetHeight() + m_BufferZone * 2.f); } // If scrolling up else if (-scroll + (float)row * spacing < 0.f) { // Scroll so the selected row is shown completely, also with m_BufferZone length to the edge. GetScrollBar(0).SetPos((float)row * spacing); } } else // autoscrolling left and right { // Get X position of position: if (m_CharacterPositions.empty()) return; float x_position = 0.f; float x_total = 0.f; if (!m_CharacterPositions.begin()->m_ListOfX.empty()) { // Get position of m_iBufferPos if ((int)m_CharacterPositions.begin()->m_ListOfX.size() >= m_iBufferPos && m_iBufferPos > 0) x_position = m_CharacterPositions.begin()->m_ListOfX[m_iBufferPos-1]; // Get complete length: x_total = m_CharacterPositions.begin()->m_ListOfX[m_CharacterPositions.begin()->m_ListOfX.size()-1]; } // Check if outside to the right if (x_position - m_HorizontalScroll + m_BufferZone * 2.f > m_CachedActualSize.GetWidth()) m_HorizontalScroll = x_position - m_CachedActualSize.GetWidth() + m_BufferZone * 2.f; // Check if outside to the left if (x_position - m_HorizontalScroll < 0.f) m_HorizontalScroll = x_position; // Check if the text doesn't even fill up to the right edge even though scrolling is done. if (m_HorizontalScroll != 0.f && x_total - m_HorizontalScroll + m_BufferZone * 2.f < m_CachedActualSize.GetWidth()) m_HorizontalScroll = x_total - m_CachedActualSize.GetWidth() + m_BufferZone * 2.f; // Now this is the fail-safe, if x_total isn't even the length of the control, // remove all scrolling if (x_total + m_BufferZone * 2.f < m_CachedActualSize.GetWidth()) m_HorizontalScroll = 0.f; } } Index: ps/trunk/source/ps/CConsole.cpp =================================================================== --- ps/trunk/source/ps/CConsole.cpp (revision 24644) +++ ps/trunk/source/ps/CConsole.cpp (revision 24645) @@ -1,699 +1,699 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* * Implements the in-game console with scripting support. */ #include "precompiled.h" #include #include "CConsole.h" #include "graphics/FontMetrics.h" #include "graphics/ShaderManager.h" #include "graphics/TextRenderer.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "gui/GUIMatrix.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/utf8.h" #include "maths/MathUtil.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "renderer/Renderer.h" #include "scriptinterface/ScriptInterface.h" CConsole* g_Console = 0; CConsole::CConsole() { m_bToggle = false; m_bVisible = false; m_fVisibleFrac = 0.0f; m_szBuffer = new wchar_t[CONSOLE_BUFFER_SIZE]; FlushBuffer(); m_iMsgHistPos = 1; m_charsPerPage = 0; m_prevTime = 0.0; m_bCursorVisState = true; m_cursorBlinkRate = 0.5; InsertMessage("[ 0 A.D. Console v0.14 ]"); InsertMessage(""); } CConsole::~CConsole() { delete[] m_szBuffer; } void CConsole::SetSize(float X, float Y, float W, float H) { m_fX = X; m_fY = Y; m_fWidth = W; m_fHeight = H; } void CConsole::UpdateScreenSize(int w, int h) { float height = h * 0.6f; SetSize(0, 0, w / g_GuiScale, height / g_GuiScale); } void CConsole::ToggleVisible() { m_bToggle = true; m_bVisible = !m_bVisible; // TODO: this should be based on input focus, not visibility if (m_bVisible) SDL_StartTextInput(); else SDL_StopTextInput(); } void CConsole::SetVisible(bool visible) { if (visible != m_bVisible) m_bToggle = true; m_bVisible = visible; if (visible) { m_prevTime = 0.0; m_bCursorVisState = false; } } void CConsole::SetCursorBlinkRate(double rate) { m_cursorBlinkRate = rate; } void CConsole::FlushBuffer() { // Clear the buffer and set the cursor and length to 0 memset(m_szBuffer, '\0', sizeof(wchar_t) * CONSOLE_BUFFER_SIZE); m_iBufferPos = m_iBufferLength = 0; } void CConsole::Update(const float deltaRealTime) { if(m_bToggle) { const float AnimateTime = .30f; const float Delta = deltaRealTime / AnimateTime; if(m_bVisible) { m_fVisibleFrac += Delta; if(m_fVisibleFrac > 1.0f) { m_fVisibleFrac = 1.0f; m_bToggle = false; } } else { m_fVisibleFrac -= Delta; if(m_fVisibleFrac < 0.0f) { m_fVisibleFrac = 0.0f; m_bToggle = false; } } } } //Render Manager. void CConsole::Render() { if (! (m_bVisible || m_bToggle) ) return; PROFILE3_GPU("console"); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); CShaderTechniquePtr solidTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid); solidTech->BeginPass(); CShaderProgramPtr solidShader = solidTech->GetShader(); CMatrix3D transform = GetDefaultGuiMatrix(); // animation: slide in from top of screen const float DeltaY = (1.0f - m_fVisibleFrac) * m_fHeight; transform.PostTranslate(m_fX, m_fY - DeltaY, 0.0f); // move to window position solidShader->Uniform(str_transform, transform); DrawWindow(solidShader); solidTech->EndPass(); CShaderTechniquePtr textTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_text); textTech->BeginPass(); CTextRenderer textRenderer(textTech->GetShader()); textRenderer.Font(CStrIntern(CONSOLE_FONT)); textRenderer.SetTransform(transform); DrawHistory(textRenderer); DrawBuffer(textRenderer); textRenderer.Render(); textTech->EndPass(); glDisable(GL_BLEND); } void CConsole::DrawWindow(CShaderProgramPtr& shader) { float boxVerts[] = { m_fWidth, 0.0f, 1.0f, 0.0f, 1.0f, m_fHeight-1.0f, m_fWidth, m_fHeight-1.0f }; shader->VertexPointer(2, GL_FLOAT, 0, boxVerts); // Draw Background // Set the color to a translucent blue shader->Uniform(str_color, 0.0f, 0.0f, 0.5f, 0.6f); shader->AssertPointersBound(); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); // Draw Border // Set the color to a translucent yellow shader->Uniform(str_color, 0.5f, 0.5f, 0.0f, 0.6f); shader->AssertPointersBound(); glDrawArrays(GL_LINE_LOOP, 0, 4); if (m_fHeight > m_iFontHeight + 4) { float lineVerts[] = { 0.0f, m_fHeight - (float)m_iFontHeight - 4.0f, m_fWidth, m_fHeight - (float)m_iFontHeight - 4.0f }; shader->VertexPointer(2, GL_FLOAT, 0, lineVerts); shader->AssertPointersBound(); glDrawArrays(GL_LINES, 0, 2); } } void CConsole::DrawHistory(CTextRenderer& textRenderer) { int i = 1; std::deque::iterator Iter; //History iterator std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory textRenderer.Color(1.0f, 1.0f, 1.0f); for (Iter = m_deqMsgHistory.begin(); Iter != m_deqMsgHistory.end() && (((i - m_iMsgHistPos + 1) * m_iFontHeight) < m_fHeight); ++Iter) { if (i >= m_iMsgHistPos) textRenderer.Put(9.0f, m_fHeight - (float)m_iFontOffset - (float)m_iFontHeight * (i - m_iMsgHistPos + 1), Iter->c_str()); i++; } } // Renders the buffer to the screen. void CConsole::DrawBuffer(CTextRenderer& textRenderer) { if (m_fHeight < m_iFontHeight) return; CMatrix3D savedTransform = textRenderer.GetTransform(); textRenderer.Translate(2.0f, m_fHeight - (float)m_iFontOffset + 1.0f, 0.0f); textRenderer.Color(1.0f, 1.0f, 0.0f); textRenderer.PutAdvance(L"]"); textRenderer.Color(1.0f, 1.0f, 1.0f); if (m_iBufferPos == 0) DrawCursor(textRenderer); for (int i = 0; i < m_iBufferLength; i++) { textRenderer.PrintfAdvance(L"%lc", m_szBuffer[i]); if (m_iBufferPos-1 == i) DrawCursor(textRenderer); } textRenderer.SetTransform(savedTransform); } void CConsole::DrawCursor(CTextRenderer& textRenderer) { if (m_cursorBlinkRate > 0.0) { // check if the cursor visibility state needs to be changed double currTime = timer_Time(); if ((currTime - m_prevTime) >= m_cursorBlinkRate) { m_bCursorVisState = !m_bCursorVisState; m_prevTime = currTime; } } else { // Should always be visible m_bCursorVisState = true; } if(m_bCursorVisState) { // Slightly translucent yellow textRenderer.Color(1.0f, 1.0f, 0.0f, 0.8f); // Cursor character is chosen to be an underscore textRenderer.Put(0.0f, 0.0f, L"_"); // Revert to the standard text color textRenderer.Color(1.0f, 1.0f, 1.0f); } } //Inserts a character into the buffer. void CConsole::InsertChar(const int szChar, const wchar_t cooked) { static int iHistoryPos = -1; if (!m_bVisible) return; switch (szChar) { case SDLK_RETURN: iHistoryPos = -1; m_iMsgHistPos = 1; ProcessBuffer(m_szBuffer); FlushBuffer(); return; case SDLK_TAB: // Auto Complete return; case SDLK_BACKSPACE: if (IsEmpty() || IsBOB()) return; if (m_iBufferPos == m_iBufferLength) m_szBuffer[m_iBufferPos - 1] = '\0'; else { for (int j = m_iBufferPos-1; j < m_iBufferLength-1; j++) m_szBuffer[j] = m_szBuffer[j+1]; // move chars to left m_szBuffer[m_iBufferLength-1] = '\0'; } m_iBufferPos--; m_iBufferLength--; return; case SDLK_DELETE: if (IsEmpty() || IsEOB()) return; if (m_iBufferPos == m_iBufferLength-1) { m_szBuffer[m_iBufferPos] = '\0'; m_iBufferLength--; } else { - if (g_keys[SDLK_RCTRL] || g_keys[SDLK_LCTRL]) + if (g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_LCTRL]) { // Make Ctrl-Delete delete up to end of line m_szBuffer[m_iBufferPos] = '\0'; m_iBufferLength = m_iBufferPos; } else { // Delete just one char and move the others left for(int j=m_iBufferPos; j lock(m_Mutex); // needed for safe access to m_deqMsgHistory int linesShown = (int)m_fHeight/m_iFontHeight - 4; m_iMsgHistPos = Clamp(static_cast(m_deqMsgHistory.size()) - linesShown, 1, static_cast(m_deqMsgHistory.size())); } else { m_iBufferPos = 0; } return; case SDLK_END: - if (g_keys[SDLK_RCTRL] || g_keys[SDLK_LCTRL]) + if (g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_LCTRL]) { m_iMsgHistPos = 1; } else { m_iBufferPos = m_iBufferLength; } return; case SDLK_LEFT: if (m_iBufferPos) m_iBufferPos--; return; case SDLK_RIGHT: if (m_iBufferPos != m_iBufferLength) m_iBufferPos++; return; // BEGIN: Buffer History Lookup case SDLK_UP: if (m_deqBufHistory.size() && iHistoryPos != (int)m_deqBufHistory.size() - 1) { iHistoryPos++; SetBuffer(m_deqBufHistory.at(iHistoryPos).c_str()); m_iBufferPos = m_iBufferLength; } return; case SDLK_DOWN: if (m_deqBufHistory.size()) { if (iHistoryPos > 0) { iHistoryPos--; SetBuffer(m_deqBufHistory.at(iHistoryPos).c_str()); m_iBufferPos = m_iBufferLength; } else if (iHistoryPos == 0) { iHistoryPos--; FlushBuffer(); } } return; // END: Buffer History Lookup // BEGIN: Message History Lookup case SDLK_PAGEUP: { std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory if (m_iMsgHistPos != (int)m_deqMsgHistory.size()) m_iMsgHistPos++; return; } case SDLK_PAGEDOWN: if (m_iMsgHistPos != 1) m_iMsgHistPos--; return; // END: Message History Lookup default: //Insert a character if (IsFull()) return; if (cooked == 0) return; if (IsEOB()) //are we at the end of the buffer? m_szBuffer[m_iBufferPos] = cooked; //cat char onto end else { //we need to insert int i; for(i=m_iBufferLength; i>m_iBufferPos; i--) m_szBuffer[i] = m_szBuffer[i-1]; // move chars to right m_szBuffer[i] = cooked; } m_iBufferPos++; m_iBufferLength++; return; } } void CConsole::InsertMessage(const std::string& message) { // (TODO: this text-wrapping is rubbish since we now use variable-width fonts) //Insert newlines to wraparound text where needed std::wstring wrapAround = wstring_from_utf8(message.c_str()); std::wstring newline(L"\n"); size_t oldNewline=0; size_t distance; //make sure everything has been initialized if ( m_charsPerPage != 0 ) { while ( oldNewline+m_charsPerPage < wrapAround.length() ) { distance = wrapAround.find(newline, oldNewline) - oldNewline; if ( distance > m_charsPerPage ) { oldNewline += m_charsPerPage; wrapAround.insert( oldNewline++, newline ); } else oldNewline += distance+1; } } // Split into lines and add each one individually oldNewline = 0; { std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory while ( (distance = wrapAround.find(newline, oldNewline)) != wrapAround.npos) { distance -= oldNewline; m_deqMsgHistory.push_front(wrapAround.substr(oldNewline, distance)); oldNewline += distance+1; } m_deqMsgHistory.push_front(wrapAround.substr(oldNewline)); } } const wchar_t* CConsole::GetBuffer() { m_szBuffer[m_iBufferLength] = 0; return( m_szBuffer ); } void CConsole::SetBuffer(const wchar_t* szMessage) { int oldBufferPos = m_iBufferPos; // remember since FlushBuffer will set it to 0 FlushBuffer(); wcsncpy(m_szBuffer, szMessage, CONSOLE_BUFFER_SIZE); m_szBuffer[CONSOLE_BUFFER_SIZE-1] = 0; m_iBufferLength = static_cast(wcslen(m_szBuffer)); m_iBufferPos = std::min(oldBufferPos, m_iBufferLength); } void CConsole::UseHistoryFile(const VfsPath& filename, int max_history_lines) { m_MaxHistoryLines = max_history_lines; m_sHistoryFile = filename; LoadHistory(); } void CConsole::ProcessBuffer(const wchar_t* szLine) { if (!szLine || wcslen(szLine) <= 0) return; ENSURE(wcslen(szLine) < CONSOLE_BUFFER_SIZE); m_deqBufHistory.push_front(szLine); SaveHistory(); // Do this each line for the moment; if a script causes // a crash it's a useful record. // Process it as JavaScript shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); ScriptRequest rq(*pScriptInterface); JS::RootedValue rval(rq.cx); pScriptInterface->Eval(CStrW(szLine).ToUTF8().c_str(), &rval); if (!rval.isUndefined()) InsertMessage(pScriptInterface->ToString(&rval)); } void CConsole::LoadHistory() { // note: we don't care if this file doesn't exist or can't be read; // just don't load anything in that case. // do this before LoadFile to avoid an error message if file not found. if (!VfsFileExists(m_sHistoryFile)) return; shared_ptr buf; size_t buflen; if (g_VFS->LoadFile(m_sHistoryFile, buf, buflen) < 0) return; CStr bytes ((char*)buf.get(), buflen); CStrW str (bytes.FromUTF8()); size_t pos = 0; while (pos != CStrW::npos) { pos = str.find('\n'); if (pos != CStrW::npos) { if (pos > 0) m_deqBufHistory.push_front(str.Left(str[pos-1] == '\r' ? pos - 1 : pos)); str = str.substr(pos + 1); } else if (str.length() > 0) m_deqBufHistory.push_front(str); } } void CConsole::SaveHistory() { WriteBuffer buffer; const int linesToSkip = (int)m_deqBufHistory.size() - m_MaxHistoryLines; std::deque::reverse_iterator it = m_deqBufHistory.rbegin(); if(linesToSkip > 0) std::advance(it, linesToSkip); for (; it != m_deqBufHistory.rend(); ++it) { CStr8 line = CStrW(*it).ToUTF8(); buffer.Append(line.data(), line.length()); static const char newline = '\n'; buffer.Append(&newline, 1); } g_VFS->CreateFile(m_sHistoryFile, buffer.Data(), buffer.Size()); } static bool isUnprintableChar(SDL_Keysym key) { switch (key.sym) { // We want to allow some, which are handled specially case SDLK_RETURN: case SDLK_TAB: case SDLK_BACKSPACE: case SDLK_DELETE: case SDLK_HOME: case SDLK_END: case SDLK_LEFT: case SDLK_RIGHT: case SDLK_UP: case SDLK_DOWN: case SDLK_PAGEUP: case SDLK_PAGEDOWN: return false; // Ignore the others default: return true; } } InReaction conInputHandler(const SDL_Event_* ev) { if (!g_Console) return IN_PASS; if (static_cast(ev->ev.type) == SDL_HOTKEYPRESS) { std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "console.toggle") { g_Console->ToggleVisible(); return IN_HANDLED; } else if (g_Console->IsActive() && hotkey == "copy") { std::string text = utf8_from_wstring(g_Console->GetBuffer()); SDL_SetClipboardText(text.c_str()); return IN_HANDLED; } else if (g_Console->IsActive() && hotkey == "paste") { char* utf8_text = SDL_GetClipboardText(); if (!utf8_text) return IN_HANDLED; std::wstring text = wstring_from_utf8(utf8_text); SDL_free(utf8_text); for (wchar_t c : text) g_Console->InsertChar(0, c); return IN_HANDLED; } } if (!g_Console->IsActive()) return IN_PASS; // In SDL2, we no longer get Unicode wchars via SDL_Keysym // we use text input events instead and they provide UTF-8 chars if (ev->ev.type == SDL_TEXTINPUT && !HotkeyIsPressed("console.toggle")) { // TODO: this could be more efficient with an interface to insert UTF-8 strings directly std::wstring wstr = wstring_from_utf8(ev->ev.text.text); for (size_t i = 0; i < wstr.length(); ++i) g_Console->InsertChar(0, wstr[i]); return IN_HANDLED; } // TODO: text editing events for IME support if (ev->ev.type != SDL_KEYDOWN) return IN_PASS; int sym = ev->ev.key.keysym.sym; // Stop unprintable characters (ctrl+, alt+ and escape), // also prevent ` and/or ~ appearing in console every time it's toggled. if (!isUnprintableChar(ev->ev.key.keysym) && !HotkeyIsPressed("console.toggle")) { g_Console->InsertChar(sym, 0); return IN_HANDLED; } return IN_PASS; } Index: ps/trunk/source/ps/Globals.cpp =================================================================== --- ps/trunk/source/ps/Globals.cpp (revision 24644) +++ ps/trunk/source/ps/Globals.cpp (revision 24645) @@ -1,96 +1,97 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "Globals.h" #include "network/NetClient.h" #include "ps/GameSetup/Config.h" #include "soundmanager/ISoundManager.h" bool g_app_minimized = false; bool g_app_has_focus = true; -std::map g_keys; +std::unordered_map g_scancodes; + int g_mouse_x = 50, g_mouse_y = 50; bool g_mouse_active = true; // g_mouse_buttons[0] is unused. The order of entries is as in KeyName.h for MOUSE_* bool g_mouse_buttons[MOUSE_LAST - MOUSE_BASE] = {0}; PIFrequencyFilter g_frequencyFilter; // updates the state of the above; never swallows messages. InReaction GlobalsInputHandler(const SDL_Event_* ev) { size_t c; switch(ev->ev.type) { case SDL_WINDOWEVENT: switch(ev->ev.window.event) { case SDL_WINDOWEVENT_MINIMIZED: g_app_minimized = true; break; case SDL_WINDOWEVENT_EXPOSED: case SDL_WINDOWEVENT_RESTORED: g_app_minimized = false; break; case SDL_WINDOWEVENT_FOCUS_GAINED: g_app_has_focus = true; break; case SDL_WINDOWEVENT_FOCUS_LOST: g_app_has_focus = false; break; case SDL_WINDOWEVENT_ENTER: g_mouse_active = true; break; case SDL_WINDOWEVENT_LEAVE: g_mouse_active = false; break; } return IN_PASS; case SDL_MOUSEMOTION: g_mouse_x = ev->ev.motion.x; g_mouse_y = ev->ev.motion.y; return IN_PASS; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: c = ev->ev.button.button; if(c < ARRAY_SIZE(g_mouse_buttons)) g_mouse_buttons[c] = (ev->ev.type == SDL_MOUSEBUTTONDOWN); else { // don't complain: just ignore people with too many mouse buttons //debug_warn(L"invalid mouse button"); } return IN_PASS; case SDL_KEYDOWN: case SDL_KEYUP: - g_keys[ev->ev.key.keysym.sym] = (ev->ev.type == SDL_KEYDOWN); + g_scancodes[ev->ev.key.keysym.scancode] = (ev->ev.type == SDL_KEYDOWN); return IN_PASS; default: return IN_PASS; } UNREACHABLE; } Index: ps/trunk/source/ps/Globals.h =================================================================== --- ps/trunk/source/ps/Globals.h (revision 24644) +++ ps/trunk/source/ps/Globals.h (revision 24645) @@ -1,63 +1,63 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_PS_GLOBALS #define INCLUDED_PS_GLOBALS #include "lib/input.h" #include "lib/frequency_filter.h" #include "ps/KeyName.h" -#include +#include // thin abstraction layer on top of SDL. // game code should use it instead of SDL_GetMouseState etc. because // Atlas does not completely emulate SDL (it can only send events). extern bool g_app_minimized; extern bool g_app_has_focus; extern int g_mouse_x, g_mouse_y; /** * Indicates whether the mouse is focused on the game window (mouse positions * should usually be considered inaccurate if this is false) */ extern bool g_mouse_active; /** - * g_keys: Key states, indexed by SDLK* constants. If an entry is true, + * g_scancodes: Key states, indexed by SDL_Scancode constants. If an entry is true, * it represents a pressed key. * Updated by GlobalsInputHandler in response to key press/release events. */ -extern std::map g_keys; +extern std::unordered_map g_scancodes; /** * g_mouse_buttons: Mouse buttons states, indexed by SDL_BUTTON_* constants. * If an entry is true, it represents a pressed button. * Updated by GlobalsInputHandler in response to mouse button up/down events. * * Be aware that SDL_BUTTON_* constants start at 1. Therefore, * g_mouse_buttons[0] is unused. The order of entries is as in KeyName.h for MOUSE_* */ extern bool g_mouse_buttons[MOUSE_LAST - MOUSE_BASE]; extern InReaction GlobalsInputHandler(const SDL_Event_* ev); extern PIFrequencyFilter g_frequencyFilter; #endif // INCLUDED_PS_GLOBALS Index: ps/trunk/source/ps/Hotkey.cpp =================================================================== --- ps/trunk/source/ps/Hotkey.cpp (revision 24644) +++ ps/trunk/source/ps/Hotkey.cpp (revision 24645) @@ -1,353 +1,308 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "Hotkey.h" #include #include "lib/external_libraries/libsdl.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/ConfigDB.h" #include "ps/Globals.h" #include "ps/KeyName.h" static bool unified[UNIFIED_LAST - UNIFIED_SHIFT]; std::unordered_map g_HotkeyMap; std::unordered_map g_HotkeyStatus; static_assert(std::is_integral::type>::value, "SDL_Scancode is not an integral enum."); static_assert(SDL_USEREVENT_ == SDL_USEREVENT, "SDL_USEREVENT_ is not the same type as the real SDL_USEREVENT"); static_assert(UNUSED_HOTKEY_CODE == SDL_SCANCODE_UNKNOWN); // Look up each key binding in the config file and set the mappings for // all key combinations that trigger it. static void LoadConfigBindings() { for (const std::pair& configPair : g_ConfigDB.GetValuesWithPrefix(CFG_COMMAND, "hotkey.")) { std::string hotkeyName = configPair.first.substr(7); // strip the "hotkey." prefix if (configPair.second.empty()) { // Unused hotkeys must still be registered in the map to appear in the hotkey editor. SHotkeyMapping unusedCode; unusedCode.name = hotkeyName; - unusedCode.negated = false; g_HotkeyMap[UNUSED_HOTKEY_CODE].push_back(unusedCode); continue; } for (const CStr& hotkey : configPair.second) { std::vector keyCombination; // Iterate through multiple-key bindings (e.g. Ctrl+I) boost::char_separator sep("+"); typedef boost::tokenizer > tokenizer; tokenizer tok(hotkey, sep); for (tokenizer::iterator it = tok.begin(); it != tok.end(); ++it) { // Attempt decode as key name SDL_Scancode scancode = FindScancode(it->c_str()); if (!scancode) { LOGWARNING("Hotkey mapping used invalid key '%s'", hotkey.c_str()); continue; } - SKey key = { scancode, false }; + SKey key = { scancode }; keyCombination.push_back(key); } std::vector::iterator itKey, itKey2; for (itKey = keyCombination.begin(); itKey != keyCombination.end(); ++itKey) { SHotkeyMapping bindCode; bindCode.name = hotkeyName; - bindCode.negated = itKey->negated; for (itKey2 = keyCombination.begin(); itKey2 != keyCombination.end(); ++itKey2) if (itKey != itKey2) // Push any auxiliary keys bindCode.requires.push_back(*itKey2); g_HotkeyMap[itKey->code].push_back(bindCode); } } } } void LoadHotkeys() { LoadConfigBindings(); - - // Set up the state of the hotkeys given no key is down. - // i.e. find those hotkeys triggered by all negations. - - for (const std::pair& p : g_HotkeyMap) - for (const SHotkeyMapping& hotkey : p.second) - { - if (!hotkey.negated) - continue; - - bool allNegated = true; - - for (const SKey& k : hotkey.requires) - if (!k.negated) - allNegated = false; - - if (allNegated) - g_HotkeyStatus[hotkey.name] = true; - } } void UnloadHotkeys() { g_HotkeyMap.clear(); g_HotkeyStatus.clear(); } -bool isNegated(const SKey& key) +bool isPressed(const SKey& key) { // Normal keycodes are below EXTRA_KEYS_BASE - if ((int)key.code < EXTRA_KEYS_BASE && g_keys[key.code] == key.negated) - return false; + if ((int)key.code < EXTRA_KEYS_BASE) + return g_scancodes[key.code]; + // Mouse 'keycodes' are after the modifier keys - else if ((int)key.code < MOUSE_LAST && (int)key.code > MOUSE_BASE && g_mouse_buttons[key.code - MOUSE_BASE] == key.negated) - return false; + else if ((int)key.code < MOUSE_LAST && (int)key.code > MOUSE_BASE) + return g_mouse_buttons[key.code - MOUSE_BASE]; + // Modifier keycodes are between the normal keys and the mouse 'keys' - else if ((int)key.code < UNIFIED_LAST && (int)key.code > SDL_NUM_SCANCODES && unified[key.code - UNIFIED_SHIFT] == key.negated) - return false; - else - return true; + else if ((int)key.code < UNIFIED_LAST && (int)key.code > SDL_NUM_SCANCODES) + return unified[key.code - UNIFIED_SHIFT]; } InReaction HotkeyStateChange(const SDL_Event_* ev) { if (ev->ev.type == SDL_HOTKEYPRESS) g_HotkeyStatus[static_cast(ev->ev.user.data1)] = true; else if (ev->ev.type == SDL_HOTKEYUP) g_HotkeyStatus[static_cast(ev->ev.user.data1)] = false; return IN_PASS; } InReaction HotkeyInputHandler(const SDL_Event_* ev) { int scancode = SDL_SCANCODE_UNKNOWN; switch(ev->ev.type) { case SDL_KEYDOWN: case SDL_KEYUP: scancode = ev->ev.key.keysym.scancode; break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: // Mousewheel events are no longer buttons, but we want to maintain the order // expected by g_mouse_buttons for compatibility if (ev->ev.button.button >= SDL_BUTTON_X1) scancode = MOUSE_BASE + (int)ev->ev.button.button + 2; else scancode = MOUSE_BASE + (int)ev->ev.button.button; break; case SDL_MOUSEWHEEL: if (ev->ev.wheel.y > 0) { scancode = MOUSE_WHEELUP; break; } else if (ev->ev.wheel.y < 0) { scancode = MOUSE_WHEELDOWN; break; } else if (ev->ev.wheel.x > 0) { scancode = MOUSE_X2; break; } else if (ev->ev.wheel.x < 0) { scancode = MOUSE_X1; break; } return IN_PASS; default: return IN_PASS; } // Somewhat hackish: // Create phantom 'unified-modifier' events when left- or right- modifier keys are pressed // Just send them to this handler; don't let the imaginary event codes leak back to real SDL. SDL_Event_ phantom; phantom.ev.type = ((ev->ev.type == SDL_KEYDOWN) || (ev->ev.type == SDL_MOUSEBUTTONDOWN)) ? SDL_KEYDOWN : SDL_KEYUP; if (phantom.ev.type == SDL_KEYDOWN) phantom.ev.key.repeat = ev->ev.type == SDL_KEYDOWN ? ev->ev.key.repeat : 0; if (scancode == SDL_SCANCODE_LSHIFT || scancode == SDL_SCANCODE_RSHIFT) { phantom.ev.key.keysym.scancode = static_cast(UNIFIED_SHIFT); unified[0] = (phantom.ev.type == SDL_KEYDOWN); HotkeyInputHandler(&phantom); } else if (scancode == SDL_SCANCODE_LCTRL || scancode == SDL_SCANCODE_RCTRL) { phantom.ev.key.keysym.scancode = static_cast(UNIFIED_CTRL); unified[1] = (phantom.ev.type == SDL_KEYDOWN); HotkeyInputHandler(&phantom); } else if (scancode == SDL_SCANCODE_LALT || scancode == SDL_SCANCODE_RALT) { phantom.ev.key.keysym.scancode = static_cast(UNIFIED_ALT); unified[2] = (phantom.ev.type == SDL_KEYDOWN); HotkeyInputHandler(&phantom); } else if (scancode == SDL_SCANCODE_LGUI || scancode == SDL_SCANCODE_RGUI) { phantom.ev.key.keysym.scancode = static_cast(UNIFIED_SUPER); unified[3] = (phantom.ev.type == SDL_KEYDOWN); HotkeyInputHandler(&phantom); } // Check whether we have any hotkeys registered for this particular keycode if (g_HotkeyMap.find(scancode) == g_HotkeyMap.end()) return (IN_PASS); // Inhibit the dispatch of hotkey events caused by real keys (not fake mouse button // events) while the console is up. bool consoleCapture = false; if (g_Console && g_Console->IsActive() && scancode < SDL_NUM_SCANCODES) consoleCapture = true; // Here's an interesting bit: // If you have an event bound to, say, 'F', and another to, say, 'Ctrl+F', pressing // 'F' while control is down would normally fire off both. // To avoid this, set the modifier keys for /all/ events this key would trigger // (Ctrl, for example, is both group-save and bookmark-save) // but only send a HotkeyPress/HotkeyDown event for the event with bindings most precisely // matching the conditions (i.e. the event with the highest number of auxiliary // keys, providing they're all down) bool typeKeyDown = ( ev->ev.type == SDL_KEYDOWN ) || ( ev->ev.type == SDL_MOUSEBUTTONDOWN ) || (ev->ev.type == SDL_MOUSEWHEEL); - // -- KEYDOWN SECTION -- - - std::vector closestMapNames; + std::vector pressedHotkeys; + std::vector releasedHotkeys; size_t closestMapMatch = 0; for (const SHotkeyMapping& hotkey : g_HotkeyMap[scancode]) { - // If a key has been pressed, and this event triggers on its release, skip it. - // Similarly, if the key's been released and the event triggers on a keypress, skip it. - if (hotkey.negated == typeKeyDown) - continue; - // Check for no unpermitted keys bool accept = true; for (const SKey& k : hotkey.requires) { - accept = isNegated(k); + accept = isPressed(k); if (!accept) break; } if (accept && !(consoleCapture && hotkey.name != "console.toggle")) { // Check if this is an equally precise or more precise match if (hotkey.requires.size() + 1 >= closestMapMatch) { // Check if more precise if (hotkey.requires.size() + 1 > closestMapMatch) { // Throw away the old less-precise matches - closestMapNames.clear(); + pressedHotkeys.clear(); + releasedHotkeys.clear(); closestMapMatch = hotkey.requires.size() + 1; } - - closestMapNames.push_back(hotkey.name.c_str()); + if (typeKeyDown) + pressedHotkeys.push_back(hotkey.name.c_str()); + else + releasedHotkeys.push_back(hotkey.name.c_str()); } } } - for (size_t i = 0; i < closestMapNames.size(); ++i) + for (const char* hotkeyName : pressedHotkeys) { - // Send a KeyPress event when a key is pressed initially and on mouseButton and mouseWheel events. + // Send a KeyPress event when a hotkey is pressed initially and on mouseButton and mouseWheel events. if (ev->ev.type != SDL_KEYDOWN || ev->ev.key.repeat == 0) { SDL_Event_ hotkeyPressNotification; hotkeyPressNotification.ev.type = SDL_HOTKEYPRESS; - hotkeyPressNotification.ev.user.data1 = const_cast(closestMapNames[i]); + hotkeyPressNotification.ev.user.data1 = const_cast(hotkeyName); in_push_priority_event(&hotkeyPressNotification); } // Send a HotkeyDown event on every key, mouseButton and mouseWheel event. // For keys the event is repeated depending on hardware and OS configured interval. // On linux, modifier keys (shift, alt, ctrl) are not repeated, see https://github.com/SFML/SFML/issues/122. SDL_Event_ hotkeyDownNotification; hotkeyDownNotification.ev.type = SDL_HOTKEYDOWN; - hotkeyDownNotification.ev.user.data1 = const_cast(closestMapNames[i]); + hotkeyDownNotification.ev.user.data1 = const_cast(hotkeyName); in_push_priority_event(&hotkeyDownNotification); } - // -- KEYUP SECTION -- - - for (const SHotkeyMapping& hotkey : g_HotkeyMap[scancode]) + for (const char* hotkeyName : releasedHotkeys) { - // If it's a keydown event, won't cause HotKeyUps in anything that doesn't - // use this key negated => skip them - // If it's a keyup event, won't cause HotKeyUps in anything that does use - // this key negated => skip them too. - if (hotkey.negated != typeKeyDown) - continue; - - // Check for no unpermitted keys - bool accept = true; - for (const SKey& k : hotkey.requires) - { - accept = isNegated(k); - if (!accept) - break; - } - - if (accept) - { - SDL_Event_ hotkeyNotification; - hotkeyNotification.ev.type = SDL_HOTKEYUP; - hotkeyNotification.ev.user.data1 = const_cast(hotkey.name.c_str()); - in_push_priority_event(&hotkeyNotification); - } + SDL_Event_ hotkeyNotification; + hotkeyNotification.ev.type = SDL_HOTKEYUP; + hotkeyNotification.ev.user.data1 = const_cast(hotkeyName); + in_push_priority_event(&hotkeyNotification); } return IN_PASS; } bool HotkeyIsPressed(const CStr& keyname) { return g_HotkeyStatus[keyname]; } Index: ps/trunk/source/ps/Hotkey.h =================================================================== --- ps/trunk/source/ps/Hotkey.h (revision 24644) +++ ps/trunk/source/ps/Hotkey.h (revision 24645) @@ -1,87 +1,84 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_HOTKEY #define INCLUDED_HOTKEY /** * @file * Hotkey system. * * Hotkeys consist of a name (an arbitrary string), and a key mapping. * The names and mappings are loaded from the config system (any * config setting with the name prefix "hotkey."). * When a hotkey is pressed one SDL_HOTKEYPRESS is triggered. While the key is * kept down repeated SDL_HOTKEYDOWN events are triggered at an interval * determined by the OS. When a hotkey is released an SDL_HOTKEYUP event is * triggered. All with the hotkey name stored in ev.user.data1 as a const char*. */ #include "CStr.h" #include "lib/input.h" #include #include // SDL_Scancode is an enum, we'll use an explicit int to avoid including SDL in this header. using SDL_Scancode_ = int; // 0x8000 is SDL_USEREVENT, this is static_asserted in Hotkey.cpp // We do this to avoid including SDL in this header. const uint SDL_USEREVENT_ = 0x8000; const uint SDL_HOTKEYPRESS = SDL_USEREVENT_; const uint SDL_HOTKEYDOWN = SDL_USEREVENT_ + 1; const uint SDL_HOTKEYUP = SDL_USEREVENT_ + 2; constexpr SDL_Scancode_ UNUSED_HOTKEY_CODE = 0; // == SDL_SCANCODE_UNKNOWN struct SKey { SDL_Scancode_ code; // scancode or MOUSE_ or UNIFIED_ value - bool negated; // whether the key must be pressed (false) or unpressed (true) - - bool operator<(const SKey& o) const { return code < o.code && negated < o.negated; } - bool operator==(const SKey& o) const { return code == o.code && negated == o.negated; } + bool operator<(const SKey& o) const { return code < o.code; } + bool operator==(const SKey& o) const { return code == o.code; } }; // Hotkey data associated with an externally-specified 'primary' keycode struct SHotkeyMapping { CStr name; // name of the hotkey - bool negated; // whether the primary key must be pressed (false) or unpressed (true) std::vector requires; // list of non-primary keys that must also be active }; typedef std::vector KeyMapping; // A mapping of scancodes onto the hotkeys that are associated with that key. // (A hotkey triggered by a combination of multiple keys will be in this map // multiple times.) extern std::unordered_map g_HotkeyMap; // The current pressed status of hotkeys extern std::unordered_map g_HotkeyStatus; extern void LoadHotkeys(); extern void UnloadHotkeys(); extern InReaction HotkeyStateChange(const SDL_Event_* ev); extern InReaction HotkeyInputHandler(const SDL_Event_* ev); extern bool HotkeyIsPressed(const CStr& keyname); #endif // INCLUDED_HOTKEY Index: ps/trunk/source/ps/scripting/JSInterface_Hotkey.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Hotkey.cpp (revision 24644) +++ ps/trunk/source/ps/scripting/JSInterface_Hotkey.cpp (revision 24645) @@ -1,173 +1,173 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "JSInterface_Hotkey.h" #include "lib/external_libraries/libsdl.h" #include "ps/CLogger.h" #include "ps/Hotkey.h" #include "ps/KeyName.h" #include "scriptinterface/ScriptConversions.h" #include #include #include /** * Convert an unordered map to a JS object, mapping keys to values. * Assumes T to have a c_str() method that returns a const char* * NB: this is unordered since no particular effort is made to preserve order. * TODO: this could be moved to ScriptConversions.cpp if the need arises. */ template static void ToJSVal_unordered_map(const ScriptRequest& rq, JS::MutableHandleValue ret, const std::unordered_map& val) { JS::RootedObject obj(rq.cx, JS_NewPlainObject(rq.cx)); if (!obj) { ret.setUndefined(); return; } for (const std::pair& item : val) { JS::RootedValue el(rq.cx); ScriptInterface::ToJSVal(rq, &el, item.second); JS_SetProperty(rq.cx, obj, item.first.c_str(), el); } ret.setObject(*obj); } template<> void ScriptInterface::ToJSVal>>>(const ScriptRequest& rq, JS::MutableHandleValue ret, const std::unordered_map>>& val) { ToJSVal_unordered_map(rq, ret, val); } template<> void ScriptInterface::ToJSVal>(const ScriptRequest& rq, JS::MutableHandleValue ret, const std::unordered_map& val) { ToJSVal_unordered_map(rq, ret, val); } /** * @return a (js) object mapping hotkey name (from cfg files) to a list ofscancode names */ JS::Value GetHotkeyMap(ScriptInterface::CmptPrivate* pCmptPrivate) { ScriptRequest rq(*pCmptPrivate->pScriptInterface); JS::RootedValue hotkeyMap(rq.cx); std::unordered_map>> hotkeys; for (const std::pair& key : g_HotkeyMap) for (const SHotkeyMapping& mapping : key.second) { std::vector keymap; if (key.first != UNUSED_HOTKEY_CODE) keymap.push_back(FindScancodeName(static_cast(key.first))); for (const SKey& secondary_key : mapping.requires) keymap.push_back(FindScancodeName(static_cast(secondary_key.code))); // If keymap is empty (== unused) or size 1, push the combination. // Otherwise, all permutations of the combination will exist, so pick one using an arbitrary order. if (keymap.size() < 2 || keymap[0] < keymap[1]) hotkeys[mapping.name].emplace_back(keymap); } pCmptPrivate->pScriptInterface->ToJSVal(rq, &hotkeyMap, hotkeys); return hotkeyMap; } /** * @return a (js) object mapping scancode names to their locale-dependent name. */ JS::Value GetScancodeKeyNames(ScriptInterface::CmptPrivate* pCmptPrivate) { ScriptRequest rq(*pCmptPrivate->pScriptInterface); JS::RootedValue obj(rq.cx); std::unordered_map map; // Get the name of all scancodes. // This is slightly wasteful but should be fine overall, they are dense. for (int i = 0; i < MOUSE_LAST; ++i) map[FindScancodeName(static_cast(i))] = FindKeyName(static_cast(i)); pCmptPrivate->pScriptInterface->ToJSVal(rq, &obj, map); return obj; } void ReloadHotkeys(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) { UnloadHotkeys(); LoadHotkeys(); } JS::Value GetConflicts(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue combination) { ScriptInterface* scriptInterface = pCmptPrivate->pScriptInterface; ScriptRequest rq(*scriptInterface); std::vector keys; if (!scriptInterface->FromJSVal(rq, combination, keys)) { LOGERROR("Invalid hotkey combination"); return JS::NullValue(); } if (keys.empty()) return JS::NullValue(); // Pick a random code as a starting point of the hotkeys (they are all equivalent). SDL_Scancode_ startCode = FindScancode(keys.back()); std::unordered_map::const_iterator it = g_HotkeyMap.find(startCode); if (it == g_HotkeyMap.end()) return JS::NullValue(); // Create a sorted vector with the remaining keys. keys.pop_back(); std::set codes; for (const std::string& key : keys) - codes.insert(SKey{ FindScancode(key), false }); + codes.insert(SKey{ FindScancode(key) }); std::vector conflicts; // This isn't very efficient, but we shouldn't iterate too many hotkeys // since we at least have one matching key. for (const SHotkeyMapping& keymap : it->second) { std::set match(keymap.requires.begin(), keymap.requires.end()); if (codes == match) conflicts.emplace_back(keymap.name); } if (conflicts.empty()) return JS::NullValue(); JS::RootedValue ret(rq.cx); scriptInterface->ToJSVal(rq, &ret, conflicts); return ret; } void JSI_Hotkey::RegisterScriptFunctions(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("GetHotkeyMap"); scriptInterface.RegisterFunction("GetScancodeKeyNames"); scriptInterface.RegisterFunction("ReloadHotkeys"); scriptInterface.RegisterFunction("GetConflicts"); }