Index: ps/trunk/source/gui/ObjectTypes/CInput.cpp =================================================================== --- ps/trunk/source/gui/ObjectTypes/CInput.cpp (revision 25179) +++ ps/trunk/source/gui/ObjectTypes/CInput.cpp (revision 25180) @@ -1,2103 +1,2109 @@ /* 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 "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_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("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: + case SDL_KEYUP: { // 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 || + // We have a probably printable key - we should return HANDLED so it can't trigger hotkeys. + // However, if Ctrl/Meta modifiers are active, just pass it through instead, + // assuming that we are indeed trying to trigger hotkeys (e.g. copy/paste). + // Escape & the "cancel" hotkey are also passed through to allow closing dialogs easily. + // See also similar logic in CConsole.cpp + // NB: this assumes that Ctrl/GUI aren't used in the Manually* functions below, + // as those code paths would obviously never be taken. + if (keyCode == SDLK_ESCAPE || EventWillFireHotkey(ev, "cancel") || g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL] || - 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); + if (ev->ev.type == SDL_KEYDOWN) + { + ManuallyImmutableHandleKeyDownEvent(keyCode); + ManuallyMutableHandleKeyDownEvent(keyCode); - UpdateBufferPositionSetting(); + 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_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_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_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().Height; rect.w = m_CachedActualSize.GetSize().Width; 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, 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, 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(); CVector2D 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/lib/input.cpp =================================================================== --- ps/trunk/source/lib/input.cpp (revision 25179) +++ ps/trunk/source/lib/input.cpp (revision 25180) @@ -1,94 +1,94 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * SDL input redirector; dispatches to multiple handlers. */ #include "precompiled.h" #include "input.h" #include "lib/external_libraries/libsdl.h" #include #include #include -const size_t MAX_HANDLERS = 9; +const size_t MAX_HANDLERS = 10; static InHandler handler_stack[MAX_HANDLERS]; static size_t handler_stack_top = 0; static std::list priority_events; void in_add_handler(InHandler handler) { ENSURE(handler); if(handler_stack_top >= MAX_HANDLERS) WARN_IF_ERR(ERR::LIMIT); handler_stack[handler_stack_top++] = handler; } void in_reset_handlers() { handler_stack_top = 0; } // send ev to each handler until one returns IN_HANDLED void in_dispatch_event(const SDL_Event_* ev) { for(int i = (int)handler_stack_top-1; i >= 0; i--) { ENSURE(handler_stack[i] && ev); InReaction ret = handler_stack[i](ev); // .. done, return if(ret == IN_HANDLED) return; // .. next handler else if(ret == IN_PASS) continue; // .. invalid return value else DEBUG_WARN_ERR(ERR::LOGIC); // invalid handler return value } } void in_push_priority_event(const SDL_Event_* event) { priority_events.push_back(*event); } int in_poll_priority_event(SDL_Event_* event) { if (priority_events.empty()) return 0; *event = priority_events.front(); priority_events.pop_front(); return 1; } int in_poll_event(SDL_Event_* event) { return in_poll_priority_event(event) ? 1 : SDL_PollEvent(&event->ev); } Index: ps/trunk/source/ps/CConsole.cpp =================================================================== --- ps/trunk/source/ps/CConsole.cpp (revision 25179) +++ ps/trunk/source/ps/CConsole.cpp (revision 25180) @@ -1,699 +1,708 @@ -/* Copyright (C) 2020 Wildfire Games. +/* 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 . */ /* * 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_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL]) { // 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_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL]) { 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; + return true; // Ignore the others default: - return true; + return false; } } 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")) + if (ev->ev.type == SDL_TEXTINPUT) { // 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) + if (ev->ev.type != SDL_KEYDOWN && ev->ev.type != SDL_KEYUP) 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) && + // Stop unprintable characters (ctrl+, alt+ and escape). + if (ev->ev.type == SDL_KEYDOWN && isUnprintableChar(ev->ev.key.keysym) && !HotkeyIsPressed("console.toggle")) { g_Console->InsertChar(sym, 0); return IN_HANDLED; } - return IN_PASS; + // We have a probably printable key - we should return HANDLED so it can't trigger hotkeys. + // However, if Ctrl/Meta modifiers are active (or it's escape), just pass it through instead, + // assuming that we are indeed trying to trigger hotkeys (e.g. copy/paste). + // Also ignore the key if we are trying to toggle the console off. + // See also similar logic in CInput.cpp + if (EventWillFireHotkey(ev, "console.toggle") || + g_scancodes[SDL_SCANCODE_LCTRL] || g_scancodes[SDL_SCANCODE_RCTRL] || + g_scancodes[SDL_SCANCODE_LGUI] || g_scancodes[SDL_SCANCODE_RGUI]) + return IN_PASS; + + return IN_HANDLED; } Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 25179) +++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 25180) @@ -1,1633 +1,1639 @@ /* 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 "lib/app_hooks.h" #include "lib/config2.h" #include "lib/input.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/external_libraries/libsdl.h" #include "lib/file/common/file_stats.h" #include "lib/res/h_mgr.h" #include "lib/res/graphics/cursor.h" #include "graphics/CinemaManager.h" #include "graphics/Color.h" #include "graphics/FontMetrics.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/MapReader.h" #include "graphics/ModelDef.h" #include "graphics/MaterialManager.h" #include "graphics/TerrainTextureManager.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "i18n/L10n.h" #include "maths/MathUtil.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "network/NetMessages.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Paths.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/HWDetect.h" #include "ps/Globals.h" #include "ps/GUID.h" #include "ps/Hotkey.h" #include "ps/Joystick.h" #include "ps/Loader.h" #include "ps/Mod.h" #include "ps/ModIo.h" #include "ps/Profile.h" #include "ps/ProfileViewer.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" // psSetLogDir #include "ps/scripting/JSInterface_Console.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/VisualReplay.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/VertexBufferManager.h" #include "renderer/ModelRenderer.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptStats.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptConversions.h" #include "simulation2/Simulation2.h" #include "lobby/IXmppClient.h" #include "soundmanager/scripting/JSInterface_Sound.h" #include "soundmanager/ISoundManager.h" #include "tools/atlas/GameInterface/GameLoop.h" #include "tools/atlas/GameInterface/View.h" #if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets #define MUST_INIT_X11 1 #include #else #define MUST_INIT_X11 0 #endif extern void RestartEngine(); #include #include #include #include ERROR_GROUP(System); ERROR_TYPE(System, SDLInitFailed); ERROR_TYPE(System, VmodeFailed); ERROR_TYPE(System, RequiredExtensionsMissing); bool g_DoRenderGui = true; bool g_DoRenderLogger = true; bool g_DoRenderCursor = true; thread_local shared_ptr g_ScriptContext; static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code static const CStr g_EventNameGameLoadProgress = "GameLoadProgress"; bool g_InDevelopmentCopy; bool g_CheckedIfInDevelopmentCopy = false; static void SetTextureQuality(int quality) { int q_flags; GLint filter; retry: // keep this in sync with SANE_TEX_QUALITY_DEFAULT switch(quality) { // worst quality case 0: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_NEAREST; break; // [perf] add bilinear filtering case 1: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] no longer reduce resolution case 2: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] add mipmaps case 3: q_flags = OGL_TEX_HALF_BPP; filter = GL_NEAREST_MIPMAP_LINEAR; break; // [perf] better filtering case 4: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [vmem] no longer reduce bpp case SANE_TEX_QUALITY_DEFAULT: q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [perf] add anisotropy case 6: // TODO: add anisotropic filtering q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // invalid default: debug_warn(L"SetTextureQuality: invalid quality"); quality = SANE_TEX_QUALITY_DEFAULT; // careful: recursion doesn't work and we don't want to duplicate // the "sane" default values. goto retry; } ogl_tex_set_defaults(q_flags, filter); } //---------------------------------------------------------------------------- // GUI integration //---------------------------------------------------------------------------- // display progress / description in loading screen void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task) { const ScriptInterface& scriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface()); ScriptRequest rq(scriptInterface); JS::RootedValueVector paramData(rq.cx); ignore_result(paramData.append(JS::NumberValue(percent))); JS::RootedValue valPendingTask(rq.cx); scriptInterface.ToJSVal(rq, &valPendingTask, pending_task); ignore_result(paramData.append(valPendingTask)); g_GUI->SendEventToAll(g_EventNameGameLoadProgress, paramData); } bool ShouldRender() { return !g_app_minimized && (g_app_has_focus || !g_VideoMode.IsInFullscreen()); } void Render() { // Do not render if not focused while in fullscreen or minimised, // as that triggers a difficult-to-reproduce crash on some graphic cards. if (!ShouldRender()) return; PROFILE3("render"); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameStart(); ogl_WarnIfError(); // prepare before starting the renderer frame if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->BeginFrame(); if (g_Game) g_Renderer.SetSimulation(g_Game->GetSimulation2()); // start new frame g_Renderer.BeginFrame(); ogl_WarnIfError(); if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->Render(); ogl_WarnIfError(); g_Renderer.RenderTextOverlays(); // If we're in Atlas game view, render special tools if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawCinemaPathTool(); ogl_WarnIfError(); } if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->GetCinema()->Render(); ogl_WarnIfError(); if (g_DoRenderGui) g_GUI->Draw(); ogl_WarnIfError(); // If we're in Atlas game view, render special overlays (e.g. editor bandbox) if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawOverlays(); ogl_WarnIfError(); } // Text: glDisable(GL_DEPTH_TEST); g_Console->Render(); ogl_WarnIfError(); if (g_DoRenderLogger) g_Logger->Render(); ogl_WarnIfError(); // Profile information g_ProfileViewer.RenderProfile(); ogl_WarnIfError(); // Draw the cursor (or set the Windows cursor, on Windows) if (g_DoRenderCursor) { PROFILE3_GPU("cursor"); CStrW cursorName = g_CursorName; if (cursorName.empty()) { cursor_draw(g_VFS, NULL, g_mouse_x, g_yres-g_mouse_y, g_GuiScale, false); } else { bool forceGL = false; CFG_GET_VAL("nohwcursor", forceGL); #if CONFIG2_GLES #warning TODO: implement cursors for GLES #else // set up transform for GL cursor glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); CMatrix3D transform; transform.SetOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f); glLoadMatrixf(&transform._11); #endif #if OS_ANDROID #warning TODO: cursors for Android #else if (cursor_draw(g_VFS, cursorName.c_str(), g_mouse_x, g_yres-g_mouse_y, g_GuiScale, forceGL) < 0) LOGWARNING("Failed to draw cursor '%s'", utf8_from_wstring(cursorName)); #endif #if CONFIG2_GLES #warning TODO: implement cursors for GLES #else // restore transform glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); #endif } } glEnable(GL_DEPTH_TEST); g_Renderer.EndFrame(); PROFILE2_ATTR("draw calls: %d", (int)g_Renderer.GetStats().m_DrawCalls); PROFILE2_ATTR("terrain tris: %d", (int)g_Renderer.GetStats().m_TerrainTris); PROFILE2_ATTR("water tris: %d", (int)g_Renderer.GetStats().m_WaterTris); PROFILE2_ATTR("model tris: %d", (int)g_Renderer.GetStats().m_ModelTris); PROFILE2_ATTR("overlay tris: %d", (int)g_Renderer.GetStats().m_OverlayTris); PROFILE2_ATTR("blend splats: %d", (int)g_Renderer.GetStats().m_BlendSplats); PROFILE2_ATTR("particles: %d", (int)g_Renderer.GetStats().m_Particles); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameEnd(); ogl_WarnIfError(); } ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED(flags)) { // If we're fullscreen, then sometimes (at least on some particular drivers on Linux) // displaying the error dialog hangs the desktop since the dialog box is behind the // fullscreen window. So we just force the game to windowed mode before displaying the dialog. // (But only if we're in the main thread, and not if we're being reentrant.) if (Threading::IsMainThread()) { static bool reentering = false; if (!reentering) { reentering = true; g_VideoMode.SetFullscreen(false); reentering = false; } } // We don't actually implement the error display here, so return appropriately return ERI_NOT_IMPLEMENTED; } const std::vector& GetMods(const CmdLineArgs& args, int flags) { const bool init_mods = (flags & INIT_MODS) == INIT_MODS; const bool add_public = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC; if (!init_mods) return g_modsLoaded; g_modsLoaded = args.GetMultiple("mod"); if (add_public) g_modsLoaded.insert(g_modsLoaded.begin(), "public"); g_modsLoaded.insert(g_modsLoaded.begin(), "mod"); return g_modsLoaded; } void MountMods(const Paths& paths, const std::vector& mods) { OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE; size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST; size_t priority = 0; for (size_t i = 0; i < mods.size(); ++i) { priority = i + 1; // Mods are higher priority than regular mountings, which default to priority 0 OsPath modName(mods[i]); // Only mount mods from the user path if they don't exist in the 'rdata' path. if (DirectoryExists(modPath / modName / "")) g_VFS->Mount(L"", modPath / modName / "", baseFlags, priority); else g_VFS->Mount(L"", modUserPath / modName / "", userFlags, priority); } // Mount the user mod last. In dev copy, mount it with a low priority. Otherwise, make it writable. g_VFS->Mount(L"", modUserPath / "user" / "", userFlags, InDevelopmentCopy() ? 0 : priority + 1); } static void InitVfs(const CmdLineArgs& args, int flags) { TIMER(L"InitVfs"); const bool setup_error = (flags & INIT_HAVE_DISPLAY_ERROR) == 0; const Paths paths(args); OsPath logs(paths.Logs()); CreateDirectories(logs, 0700); psSetLogDir(logs); // desired location for crashlog is now known. update AppHooks ASAP // (particularly before the following error-prone operations): AppHooks hooks = {0}; hooks.bundle_logs = psBundleLogs; hooks.get_log_dir = psLogDir; if (setup_error) hooks.display_error = psDisplayError; app_hooks_update(&hooks); g_VFS = CreateVfs(); const OsPath readonlyConfig = paths.RData()/"config"/""; // Mount these dirs with highest priority so that mods can't overwrite them. g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE, VFS_MAX_PRIORITY); // (adding XMBs to archive speeds up subsequent reads) if (readonlyConfig != paths.Config()) g_VFS->Mount(L"config/", readonlyConfig, 0, VFS_MAX_PRIORITY-1); g_VFS->Mount(L"config/", paths.Config(), 0, VFS_MAX_PRIORITY); g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"", 0, VFS_MAX_PRIORITY); g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH, VFS_MAX_PRIORITY); // Engine localization files (regular priority, these can be overwritten). g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/""); MountMods(paths, GetMods(args, flags)); // note: don't bother with g_VFS->TextRepresentation - directories // haven't yet been populated and are empty. } static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcScriptInterface, JS::HandleValue initData) { { // console TIMER(L"ps_console"); g_Console->UpdateScreenSize(g_xres, g_yres); // Calculate and store the line spacing CFontMetrics font(CStrIntern(CONSOLE_FONT)); g_Console->m_iFontHeight = font.GetLineSpacing(); g_Console->m_iFontWidth = font.GetCharacterWidth(L'C'); g_Console->m_charsPerPage = (size_t)(g_xres / g_Console->m_iFontWidth); // Offset by an arbitrary amount, to make it fit more nicely g_Console->m_iFontOffset = 7; double blinkRate = 0.5; CFG_GET_VAL("gui.cursorblinkrate", blinkRate); g_Console->SetCursorBlinkRate(blinkRate); } // hotkeys { TIMER(L"ps_lang_hotkeys"); LoadHotkeys(g_ConfigDB); } if (!setup_gui) { // We do actually need *some* kind of GUI loaded, so use the // (currently empty) Atlas one g_GUI->SwitchPage(L"page_atlas.xml", srcScriptInterface, initData); return; } // GUI uses VFS, so this must come after VFS init. g_GUI->SwitchPage(gui_page, srcScriptInterface, initData); } void InitPsAutostart(bool networked, JS::HandleValue attrs) { // The GUI has not been initialized yet, so use the simulation scriptinterface for this variable ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue playerAssignments(rq.cx); ScriptInterface::CreateObject(rq, &playerAssignments); if (!networked) { JS::RootedValue localPlayer(rq.cx); ScriptInterface::CreateObject(rq, &localPlayer, "player", g_Game->GetPlayerID()); scriptInterface.SetProperty(playerAssignments, "local", localPlayer); } JS::RootedValue sessionInitData(rq.cx); ScriptInterface::CreateObject( rq, &sessionInitData, "attribs", attrs, "playerAssignments", playerAssignments); InitPs(true, L"page_loading.xml", &scriptInterface, sessionInitData); } void InitInput() { g_Joystick.Initialise(); // register input handlers // This stack is constructed so the first added, will be the last // one called. This is important, because each of the handlers // has the potential to block events to go further down // in the chain. I.e. the last one in the list added, is the // only handler that can block all messages before they are // processed. in_add_handler(game_view_handler); in_add_handler(CProfileViewer::InputThunk); - in_add_handler(HotkeyInputHandler); + in_add_handler(HotkeyInputActualHandler); // gui_handler needs to be registered after (i.e. called before!) the // hotkey handler so that input boxes can be typed in without // setting off hotkeys. in_add_handler(gui_handler); // Likewise for the console. in_add_handler(conInputHandler); in_add_handler(touch_input_handler); - // must be registered after (called before) the GUI which relies on these globals + // Should be called after scancode map update (i.e. after the global input, but before UI). + // This never blocks the event, but it does some processing necessary for hotkeys, + // which are triggered later down the input chain. + // (by calling this before the UI, we can use 'EventWouldTriggerHotkey' in the UI). + in_add_handler(HotkeyInputPrepHandler); + + // These two must be called first (i.e. pushed last) + // GlobalsInputHandler deals with some important global state, + // such as which scancodes are being pressed, mouse buttons pressed, etc. + // while HotkeyStateChange updates the map of active hotkeys. in_add_handler(GlobalsInputHandler); - - // Should be called first, this updates our hotkey press state - // so that js calls to HotkeyIsPressed are synched with events. in_add_handler(HotkeyStateChange); } static void ShutdownPs() { SAFE_DELETE(g_GUI); UnloadHotkeys(); // disable the special Windows cursor, or free textures for OGL cursors cursor_draw(g_VFS, 0, g_mouse_x, g_yres-g_mouse_y, 1.0, false); } static void InitRenderer() { TIMER(L"InitRenderer"); // create renderer new CRenderer; // create terrain related stuff new CTerrainTextureManager; g_Renderer.Open(g_xres, g_yres); // Setup lighting environment. Since the Renderer accesses the // lighting environment through a pointer, this has to be done before // the first Frame. g_Renderer.SetLightEnv(&g_LightEnv); // I haven't seen the camera affecting GUI rendering and such, but the // viewport has to be updated according to the video mode SViewPort vp; vp.m_X = 0; vp.m_Y = 0; vp.m_Width = g_xres; vp.m_Height = g_yres; g_Renderer.SetViewport(vp); ModelDefActivateFastImpl(); ColorActivateFastImpl(); ModelRenderer::Init(); } static void InitSDL() { #if OS_LINUX // In fullscreen mode when SDL is compiled with DGA support, the mouse // sensitivity often appears to be unusably wrong (typically too low). // (This seems to be reported almost exclusively on Ubuntu, but can be // reproduced on Gentoo after explicitly enabling DGA.) // Disabling the DGA mouse appears to fix that problem, and doesn't // have any obvious negative effects. setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0); #endif if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_NOPARACHUTE) < 0) { LOGERROR("SDL library initialization failed: %s", SDL_GetError()); throw PSERROR_System_SDLInitFailed(); } atexit(SDL_Quit); // Text input is active by default, disable it until it is actually needed. SDL_StopTextInput(); #if SDL_VERSION_ATLEAST(2, 0, 9) // SDL2 >= 2.0.9 defaults to 32 pixels (to support touch screens) but that can break our double-clicking. SDL_SetHint(SDL_HINT_MOUSE_DOUBLE_CLICK_RADIUS, "1"); #endif #if OS_MACOSX // Some Mac mice only have one button, so they can't right-click // but SDL2 can emulate that with Ctrl+Click bool macMouse = false; CFG_GET_VAL("macmouse", macMouse); SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0"); #endif } static void ShutdownSDL() { SDL_Quit(); } void EndGame() { SAFE_DELETE(g_NetClient); SAFE_DELETE(g_NetServer); SAFE_DELETE(g_Game); if (CRenderer::IsInitialised()) { ISoundManager::CloseGame(); g_Renderer.ResetState(); } } void Shutdown(int flags) { const bool hasRenderer = CRenderer::IsInitialised(); if ((flags & SHUTDOWN_FROM_CONFIG)) goto from_config; EndGame(); SAFE_DELETE(g_XmppClient); SAFE_DELETE(g_ModIo); ShutdownPs(); TIMER_BEGIN(L"shutdown TexMan"); delete &g_TexMan; TIMER_END(L"shutdown TexMan"); if (hasRenderer) { TIMER_BEGIN(L"shutdown Renderer"); g_Renderer.~CRenderer(); g_VBMan.Shutdown(); TIMER_END(L"shutdown Renderer"); } g_RenderingOptions.ClearHooks(); g_Profiler2.ShutdownGPU(); // Free cursors before shutting down SDL, as they may depend on SDL. cursor_shutdown(); TIMER_BEGIN(L"shutdown SDL"); ShutdownSDL(); TIMER_END(L"shutdown SDL"); if (hasRenderer) g_VideoMode.Shutdown(); TIMER_BEGIN(L"shutdown UserReporter"); g_UserReporter.Deinitialize(); TIMER_END(L"shutdown UserReporter"); // Cleanup curl now that g_ModIo and g_UserReporter have been shutdown. curl_global_cleanup(); delete &g_L10n; from_config: TIMER_BEGIN(L"shutdown ConfigDB"); delete &g_ConfigDB; TIMER_END(L"shutdown ConfigDB"); SAFE_DELETE(g_Console); // This is needed to ensure that no callbacks from the JSAPI try to use // the profiler when it's already destructed g_ScriptContext.reset(); // resource // first shut down all resource owners, and then the handle manager. TIMER_BEGIN(L"resource modules"); ISoundManager::SetEnabled(false); g_VFS.reset(); // this forcibly frees all open handles (thus preventing real leaks), // and makes further access to h_mgr impossible. h_mgr_shutdown(); file_stats_dump(); TIMER_END(L"resource modules"); TIMER_BEGIN(L"shutdown misc"); timer_DisplayClientTotals(); CNetHost::Deinitialize(); // should be last, since the above use them SAFE_DELETE(g_Logger); delete &g_Profiler; delete &g_ProfileViewer; SAFE_DELETE(g_ScriptStatsTable); TIMER_END(L"shutdown misc"); } #if OS_UNIX static void FixLocales() { #if OS_MACOSX || OS_BSD // OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle // wide characters. Peculiarly the string "UTF-8" seems to be acceptable // despite not being a real locale, and it's conveniently language-agnostic, // so use that. setlocale(LC_CTYPE, "UTF-8"); #endif // On misconfigured systems with incorrect locale settings, we'll die // with a C++ exception when some code (e.g. Boost) tries to use locales. // To avoid death, we'll detect the problem here and warn the user and // reset to the default C locale. // For informing the user of the problem, use the list of env vars that // glibc setlocale looks at. (LC_ALL is checked first, and LANG last.) const char* const LocaleEnvVars[] = { "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LC_MESSAGES", "LANG" }; try { // this constructor is similar to setlocale(LC_ALL, ""), // but instead of returning NULL, it throws runtime_error // when the first locale env variable found contains an invalid value std::locale(""); } catch (std::runtime_error&) { LOGWARNING("Invalid locale settings"); for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++) { if (char* envval = getenv(LocaleEnvVars[i])) LOGWARNING(" %s=\"%s\"", LocaleEnvVars[i], envval); else LOGWARNING(" %s=\"(unset)\"", LocaleEnvVars[i]); } // We should set LC_ALL since it overrides LANG if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1)) debug_warn(L"Invalid locale settings, and unable to set LC_ALL env variable."); else LOGWARNING("Setting LC_ALL env variable to: %s", getenv("LC_ALL")); } } #else static void FixLocales() { // Do nothing on Windows } #endif void EarlyInit() { // If you ever want to catch a particular allocation: //_CrtSetBreakAlloc(232647); Threading::SetMainThread(); debug_SetThreadName("main"); // add all debug_printf "tags" that we are interested in: debug_filter_add("TIMER"); timer_Init(); // initialise profiler early so it can profile startup, // but only after LatchStartTime g_Profiler2.Initialise(); FixLocales(); // Because we do GL calls from a secondary thread, Xlib needs to // be told to support multiple threads safely. // This is needed for Atlas, but we have to call it before any other // Xlib functions (e.g. the ones used when drawing the main menu // before launching Atlas) #if MUST_INIT_X11 int status = XInitThreads(); if (status == 0) debug_printf("Error enabling thread-safety via XInitThreads\n"); #endif // Initialise the low-quality rand function srand(time(NULL)); // NOTE: this rand should *not* be used for simulation! } bool Autostart(const CmdLineArgs& args); /** * Returns true if the user has intended to start a visual replay from command line. */ bool AutostartVisualReplay(const std::string& replayFile); bool Init(const CmdLineArgs& args, int flags) { h_mgr_init(); // Do this as soon as possible, because it chdirs // and will mess up the error reporting if anything // crashes before the working directory is set. InitVfs(args, flags); // This must come after VFS init, which sets the current directory // (required for finding our output log files). g_Logger = new CLogger; new CProfileViewer; new CProfileManager; // before any script code g_ScriptStatsTable = new CScriptStatsTable; g_ProfileViewer.AddRootTable(g_ScriptStatsTable); // Set up the console early, so that debugging // messages can be logged to it. (The console's size // and fonts are set later in InitPs()) g_Console = new CConsole(); // g_ConfigDB, command line args, globals CONFIG_Init(args); // Using a global object for the context is a workaround until Simulation and AI use // their own threads and also their own contexts. const int contextSize = 384 * 1024 * 1024; const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024; g_ScriptContext = ScriptContext::CreateContext(contextSize, heapGrowthBytesGCTrigger); Mod::CacheEnabledModVersions(g_ScriptContext); // Special command-line mode to dump the entity schemas instead of running the game. // (This must be done after loading VFS etc, but should be done before wasting time // on anything else.) if (args.Has("dumpSchema")) { CSimulation2 sim(NULL, g_ScriptContext, NULL); sim.LoadDefaultScripts(); std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc); f << sim.GenerateSchema(); std::cout << "Generated entity.rng\n"; exit(0); } CNetHost::Initialize(); #if CONFIG2_AUDIO if (!args.Has("autostart-nonvisual") && !g_DisableAudio) ISoundManager::CreateSoundManager(); #endif // Check if there are mods specified on the command line, // or if we already set the mods (~INIT_MODS), // else check if there are mods that should be loaded specified // in the config and load those (by aborting init and restarting // the engine). if (!args.Has("mod") && (flags & INIT_MODS) == INIT_MODS) { CStr modstring; CFG_GET_VAL("mod.enabledmods", modstring); if (!modstring.empty()) { std::vector mods; boost::split(mods, modstring, boost::is_any_of(" "), boost::token_compress_on); std::swap(g_modsLoaded, mods); // Abort init and restart RestartEngine(); return false; } } new L10n; // Optionally start profiler HTTP output automatically // (By default it's only enabled by a hotkey, for security/performance) bool profilerHTTPEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerHTTPEnable); if (profilerHTTPEnable) g_Profiler2.EnableHTTP(); // Initialise everything except Win32 sockets (because our networking // system already inits those) curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32); if (!g_Quickstart) g_UserReporter.Initialize(); // after config PROFILE2_EVENT("Init finished"); return true; } void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods) { const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0; if(setup_vmode) { InitSDL(); if (!g_VideoMode.InitSDL()) throw PSERROR_System_VmodeFailed(); // abort startup } RunHardwareDetection(); const int quality = SANE_TEX_QUALITY_DEFAULT; // TODO: set value from config file SetTextureQuality(quality); ogl_WarnIfError(); // Optionally start profiler GPU timings automatically // (By default it's only enabled by a hotkey, for performance/compatibility) bool profilerGPUEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerGPUEnable); if (profilerGPUEnable) g_Profiler2.EnableGPU(); if(!g_Quickstart) { WriteSystemInfo(); // note: no longer vfs_display here. it's dog-slow due to unbuffered // file output and very rarely needed. } if(g_DisableAudio) ISoundManager::SetEnabled(false); g_GUI = new CGUIManager(); // (must come after SetVideoMode, since it calls ogl_Init) CStr8 renderPath = "default"; CFG_GET_VAL("renderpath", renderPath); if ((ogl_HaveExtensions(0, "GL_ARB_vertex_program", "GL_ARB_fragment_program", NULL) != 0 // ARB && ogl_HaveExtensions(0, "GL_ARB_vertex_shader", "GL_ARB_fragment_shader", NULL) != 0) // GLSL || RenderPathEnum::FromString(renderPath) == FIXED) { // It doesn't make sense to continue working here, because we're not // able to display anything. DEBUG_DISPLAY_FATAL_ERROR( L"Your graphics card doesn't appear to be fully compatible with OpenGL shaders." L" The game does not support pre-shader graphics cards." L" You are advised to try installing newer drivers and/or upgrade your graphics card." L" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734" ); } const char* missing = ogl_HaveExtensions(0, "GL_ARB_multitexture", "GL_EXT_draw_range_elements", "GL_ARB_texture_env_combine", "GL_ARB_texture_env_dot3", NULL); if(missing) { wchar_t buf[500]; swprintf_s(buf, ARRAY_SIZE(buf), L"The %hs extension doesn't appear to be available on your computer." L" The game may still work, though - you are welcome to try at your own risk." L" If not or it doesn't look right, upgrade your graphics card.", missing ); DEBUG_DISPLAY_ERROR(buf); // TODO: i18n } if (!ogl_HaveExtension("GL_ARB_texture_env_crossbar")) { DEBUG_DISPLAY_ERROR( L"The GL_ARB_texture_env_crossbar extension doesn't appear to be available on your computer." L" Shadows are not available and overall graphics quality might suffer." L" You are advised to try installing newer drivers and/or upgrade your graphics card."); g_ConfigDB.SetValueBool(CFG_HWDETECT, "shadows", false); } ogl_WarnIfError(); g_RenderingOptions.ReadConfigAndSetupHooks(); InitRenderer(); InitInput(); ogl_WarnIfError(); // TODO: Is this the best place for this? if (VfsDirectoryExists(L"maps/")) CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng"); try { if (!AutostartVisualReplay(args.Get("replay-visual")) && !Autostart(args)) { const bool setup_gui = ((flags & INIT_NO_GUI) == 0); // We only want to display the splash screen at startup shared_ptr scriptInterface = g_GUI->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue data(rq.cx); if (g_GUI) { ScriptInterface::CreateObject(rq, &data, "isStartup", true); if (!installedMods.empty()) scriptInterface->SetProperty(data, "installedMods", installedMods); } InitPs(setup_gui, installedMods.empty() ? L"page_pregame.xml" : L"page_modmod.xml", g_GUI->GetScriptInterface().get(), data); } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map Loading failed // Start the engine so we have a GUI InitPs(true, L"page_pregame.xml", NULL, JS::UndefinedHandleValue); // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } } void InitNonVisual(const CmdLineArgs& args) { // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); Autostart(args); } void RenderGui(bool RenderingState) { g_DoRenderGui = RenderingState; } void RenderLogger(bool RenderingState) { g_DoRenderLogger = RenderingState; } void RenderCursor(bool RenderingState) { g_DoRenderCursor = RenderingState; } /** * Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON * data from it. * The scenario map format is used for scenario and skirmish map types (random * games do not use a "map" (format) but a small JavaScript program which * creates a map on the fly). It contains a section to initialize the game * setup screen. * @param mapPath Absolute path (from VFS root) to the map file to peek in. * @return ScriptSettings in JSON format extracted from the map. */ CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath) { CXeromyces mapFile; const char *pathToSettings[] = { "Scenario", "ScriptSettings", "" // Path to JSON data in map }; Status loadResult = mapFile.Load(g_VFS, mapPath); if (INFO::OK != loadResult) { LOGERROR("LoadSettingsOfScenarioMap: Unable to load map file '%s'", mapPath.string8()); throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos."); } XMBElement mapElement = mapFile.GetRoot(); // Select the ScriptSettings node in the map file... for (int i = 0; pathToSettings[i][0]; ++i) { int childId = mapFile.GetElementID(pathToSettings[i]); XMBElementList nodes = mapElement.GetChildNodes(); auto it = std::find_if(nodes.begin(), nodes.end(), [&childId](const XMBElement& child) { return child.GetNodeName() == childId; }); if (it != nodes.end()) mapElement = *it; } // ... they contain a JSON document to initialize the game setup // screen return mapElement.GetText(); } /* * Command line options for autostart * (keep synchronized with binaries/system/readme.txt): * * -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME; * TYPEDIR is skirmishes, scenarios, or random * -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random) * -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra) * -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI * (0: sandbox, 5: very hard) * -autostart-aiseed=AISEED sets the seed used for the AI random * generator (default 0, use -1 for random) * -autostart-player=NUMBER sets the playerID in non-networked games (default 1, use -1 for observer) * -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV * (skirmish and random maps only) * -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2). * -autostart-ceasefire=NUM sets a ceasefire duration NUM * (default 0 minutes) * -autostart-nonvisual disable any graphics and sounds * -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME * located in simulation/data/settings/victory_conditions/ * (default conquest). When the first given SCRIPTNAME is * "endless", no victory conditions will apply. * -autostart-wonderduration=NUM sets the victory duration NUM for wonder victory condition * (default 10 minutes) * -autostart-relicduration=NUM sets the victory duration NUM for relic victory condition * (default 10 minutes) * -autostart-reliccount=NUM sets the number of relics for relic victory condition * (default 2 relics) * -autostart-disable-replay disable saving of replays * * Multiplayer: * -autostart-playername=NAME sets local player NAME (default 'anonymous') * -autostart-host sets multiplayer host mode * -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer * game (default 2) * -autostart-client=IP sets multiplayer client to join host at * given IP address * Random maps only: * -autostart-size=TILES sets random map size in TILES (default 192) * -autostart-players=NUMBER sets NUMBER of players on random map * (default 2) * * Examples: * 1) "Bob" will host a 2 player game on the Arcadia map: * -autostart="scenarios/Arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob" * "Alice" joins the match as player 2: * -autostart="scenarios/Arcadia" -autostart-client=127.0.0.1 -autostart-playername="Alice" * The players use the developer overlay to control players. * * 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot: * -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra * * 3) Observe the PetraBot on a triggerscript map: * -autostart="random/jebel_barkal" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=1:petra -autostart-ai=2:petra -autostart-player=-1 */ bool Autostart(const CmdLineArgs& args) { CStr autoStartName = args.Get("autostart"); if (autoStartName.empty()) return false; g_Game = new CGame(!args.Has("autostart-disable-replay")); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue attrs(rq.cx); JS::RootedValue settings(rq.cx); JS::RootedValue playerData(rq.cx); ScriptInterface::CreateObject(rq, &attrs); ScriptInterface::CreateObject(rq, &settings); ScriptInterface::CreateArray(rq, &playerData); // The directory in front of the actual map name indicates which type // of map is being loaded. Drawback of this approach is the association // of map types and folders is hard-coded, but benefits are: // - No need to pass the map type via command line separately // - Prevents mixing up of scenarios and skirmish maps to some degree Path mapPath = Path(autoStartName); std::wstring mapDirectory = mapPath.Parent().Filename().string(); std::string mapType; if (mapDirectory == L"random") { // Random map definition will be loaded from JSON file, so we need to parse it std::wstring scriptPath = L"maps/" + autoStartName.FromUTF8() + L".json"; JS::RootedValue scriptData(rq.cx); scriptInterface.ReadJSONFile(scriptPath, &scriptData); if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "settings", &settings)) { // JSON loaded ok - copy script name over to game attributes std::wstring scriptFile; scriptInterface.GetProperty(settings, "Script", scriptFile); scriptInterface.SetProperty(attrs, "script", scriptFile); // RMS filename } else { // Problem with JSON file LOGERROR("Autostart: Error reading random map script '%s'", utf8_from_wstring(scriptPath)); throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details."); } // Get optional map size argument (default 192) uint mapSize = 192; if (args.Has("autostart-size")) { CStr size = args.Get("autostart-size"); mapSize = size.ToUInt(); } scriptInterface.SetProperty(settings, "Size", mapSize); // Random map size (in patches) // Get optional number of players (default 2) size_t numPlayers = 2; if (args.Has("autostart-players")) { CStr num = args.Get("autostart-players"); numPlayers = num.ToUInt(); } // Set up player data for (size_t i = 0; i < numPlayers; ++i) { JS::RootedValue player(rq.cx); // We could load player_defaults.json here, but that would complicate the logic // even more and autostart is only intended for developers anyway ScriptInterface::CreateObject(rq, &player, "Civ", "athen"); scriptInterface.SetPropertyInt(playerData, i, player); } mapType = "random"; } else if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // Initialize general settings from the map data so some values // (e.g. name of map) are always present, even when autostart is // partially configured CStr8 mapSettingsJSON = LoadSettingsOfScenarioMap("maps/" + autoStartName + ".xml"); scriptInterface.ParseJSON(mapSettingsJSON, &settings); // Initialize the playerData array being modified by autostart // with the real map data, so sensible values are present: scriptInterface.GetProperty(settings, "PlayerData", &playerData); if (mapDirectory == L"scenarios") mapType = "scenario"; else mapType = "skirmish"; } else { LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory)); throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types."); } scriptInterface.SetProperty(attrs, "mapType", mapType); scriptInterface.SetProperty(attrs, "map", "maps/" + autoStartName); scriptInterface.SetProperty(settings, "mapType", mapType); scriptInterface.SetProperty(settings, "CheatsEnabled", true); // The seed is used for both random map generation and simulation u32 seed = 0; if (args.Has("autostart-seed")) { CStr seedArg = args.Get("autostart-seed"); if (seedArg == "-1") seed = rand(); else seed = seedArg.ToULong(); } scriptInterface.SetProperty(settings, "Seed", seed); // Set seed for AIs u32 aiseed = 0; if (args.Has("autostart-aiseed")) { CStr seedArg = args.Get("autostart-aiseed"); if (seedArg == "-1") aiseed = rand(); else aiseed = seedArg.ToULong(); } scriptInterface.SetProperty(settings, "AISeed", aiseed); // Set player data for AIs // attrs.settings = { PlayerData: [ { AI: ... }, ... ] } // or = { PlayerData: [ null, { AI: ... }, ... ] } when gaia set int offset = 1; JS::RootedValue player(rq.cx); if (scriptInterface.GetPropertyInt(playerData, 0, &player) && player.isNull()) offset = 0; // Set teams if (args.Has("autostart-team")) { std::vector civArgs = args.GetMultiple("autostart-team"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-team option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } int teamID = civArgs[i].AfterFirst(":").ToInt() - 1; scriptInterface.SetProperty(currentPlayer, "Team", teamID); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } int ceasefire = 0; if (args.Has("autostart-ceasefire")) ceasefire = args.Get("autostart-ceasefire").ToInt(); scriptInterface.SetProperty(settings, "Ceasefire", ceasefire); if (args.Has("autostart-ai")) { std::vector aiArgs = args.GetMultiple("autostart-ai"); for (size_t i = 0; i < aiArgs.size(); ++i) { int playerID = aiArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-ai option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } scriptInterface.SetProperty(currentPlayer, "AI", aiArgs[i].AfterFirst(":")); scriptInterface.SetProperty(currentPlayer, "AIDiff", 3); scriptInterface.SetProperty(currentPlayer, "AIBehavior", "balanced"); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } // Set AI difficulty if (args.Has("autostart-aidiff")) { std::vector civArgs = args.GetMultiple("autostart-aidiff"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-aidiff option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } scriptInterface.SetProperty(currentPlayer, "AIDiff", civArgs[i].AfterFirst(":").ToInt()); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } // Set player data for Civs if (args.Has("autostart-civ")) { if (mapDirectory != L"scenarios") { std::vector civArgs = args.GetMultiple("autostart-civ"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-civ option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } scriptInterface.SetProperty(currentPlayer, "Civ", civArgs[i].AfterFirst(":")); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } else LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios"); } // Add player data to map settings scriptInterface.SetProperty(settings, "PlayerData", playerData); // Add map settings to game attributes scriptInterface.SetProperty(attrs, "settings", settings); // Get optional playername CStrW userName = L"anonymous"; if (args.Has("autostart-playername")) userName = args.Get("autostart-playername").FromUTF8(); // Add additional scripts to the TriggerScripts property std::vector triggerScriptsVector; JS::RootedValue triggerScripts(rq.cx); if (scriptInterface.HasProperty(settings, "TriggerScripts")) { scriptInterface.GetProperty(settings, "TriggerScripts", &triggerScripts); FromJSVal_vector(rq, triggerScripts, triggerScriptsVector); } if (!CRenderer::IsInitialised()) { CStr nonVisualScript = "scripts/NonVisualTrigger.js"; triggerScriptsVector.push_back(nonVisualScript.FromUTF8()); } std::vector victoryConditions(1, "conquest"); if (args.Has("autostart-victory")) victoryConditions = args.GetMultiple("autostart-victory"); if (victoryConditions.size() == 1 && victoryConditions[0] == "endless") victoryConditions.clear(); scriptInterface.SetProperty(settings, "VictoryConditions", victoryConditions); for (const CStr& victory : victoryConditions) { JS::RootedValue scriptData(rq.cx); JS::RootedValue data(rq.cx); JS::RootedValue victoryScripts(rq.cx); CStrW scriptPath = L"simulation/data/settings/victory_conditions/" + victory.FromUTF8() + L".json"; scriptInterface.ReadJSONFile(scriptPath, &scriptData); if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "Data", &data) && !data.isUndefined() && scriptInterface.GetProperty(data, "Scripts", &victoryScripts) && !victoryScripts.isUndefined()) { std::vector victoryScriptsVector; FromJSVal_vector(rq, victoryScripts, victoryScriptsVector); triggerScriptsVector.insert(triggerScriptsVector.end(), victoryScriptsVector.begin(), victoryScriptsVector.end()); } else { LOGERROR("Autostart: Error reading victory script '%s'", utf8_from_wstring(scriptPath)); throw PSERROR_Game_World_MapLoadFailed("Error reading victory script.\nCheck application log for details."); } } ToJSVal_vector(rq, &triggerScripts, triggerScriptsVector); scriptInterface.SetProperty(settings, "TriggerScripts", triggerScripts); int wonderDuration = 10; if (args.Has("autostart-wonderduration")) wonderDuration = args.Get("autostart-wonderduration").ToInt(); scriptInterface.SetProperty(settings, "WonderDuration", wonderDuration); int relicDuration = 10; if (args.Has("autostart-relicduration")) relicDuration = args.Get("autostart-relicduration").ToInt(); scriptInterface.SetProperty(settings, "RelicDuration", relicDuration); int relicCount = 2; if (args.Has("autostart-reliccount")) relicCount = args.Get("autostart-reliccount").ToInt(); scriptInterface.SetProperty(settings, "RelicCount", relicCount); if (args.Has("autostart-host")) { InitPsAutostart(true, attrs); size_t maxPlayers = 2; if (args.Has("autostart-host-players")) maxPlayers = args.Get("autostart-host-players").ToUInt(); // Generate a secret to identify the host client. std::string secret = ps_generate_guid(); g_NetServer = new CNetServer(false, maxPlayers); g_NetServer->SetControllerSecret(secret); g_NetServer->UpdateInitAttributes(&attrs, scriptInterface); bool ok = g_NetServer->SetupConnection(PS_DEFAULT_PORT); ENSURE(ok); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(userName); g_NetClient->SetupServerData("127.0.0.1", PS_DEFAULT_PORT, false); g_NetClient->SetControllerSecret(secret); g_NetClient->SetupConnection(nullptr); } else if (args.Has("autostart-client")) { InitPsAutostart(true, attrs); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(userName); CStr ip = args.Get("autostart-client"); if (ip.empty()) ip = "127.0.0.1"; g_NetClient->SetupServerData(ip, PS_DEFAULT_PORT, false); ENSURE(g_NetClient->SetupConnection(nullptr)); } else { g_Game->SetPlayerID(args.Has("autostart-player") ? args.Get("autostart-player").ToInt() : 1); g_Game->StartGame(&attrs, ""); if (CRenderer::IsInitialised()) { InitPsAutostart(false, attrs); } else { // TODO: Non progressive load can fail - need a decent way to handle this LDR_NonprogressiveLoad(); ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK); } } return true; } bool AutostartVisualReplay(const std::string& replayFile) { if (!FileExists(OsPath(replayFile))) return false; g_Game = new CGame(false); g_Game->SetPlayerID(-1); g_Game->StartVisualReplay(replayFile); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue attrs(rq.cx, g_Game->GetSimulation2()->GetInitAttributes()); InitPsAutostart(false, attrs); return true; } void CancelLoad(const CStrW& message) { shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); ScriptRequest rq(pScriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); LDR_Cancel(); if (g_GUI && g_GUI->GetPageCount() && pScriptInterface->HasProperty(global, "cancelOnLoadGameError")) pScriptInterface->CallFunctionVoid(global, "cancelOnLoadGameError", message); } bool InDevelopmentCopy() { if (!g_CheckedIfInDevelopmentCopy) { g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK); g_CheckedIfInDevelopmentCopy = true; } return g_InDevelopmentCopy; } Index: ps/trunk/source/ps/Hotkey.cpp =================================================================== --- ps/trunk/source/ps/Hotkey.cpp (revision 25179) +++ ps/trunk/source/ps/Hotkey.cpp (revision 25180) @@ -1,430 +1,466 @@ /* 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; namespace { std::unordered_map g_HotkeyStatus; struct PressedHotkey { PressedHotkey(const SHotkeyMapping* m, bool t) : mapping(m), retriggered(t) {}; // NB: this points to one of g_HotkeyMap's mappings. It works because that std::unordered_map is stable once constructed. const SHotkeyMapping* mapping; // Whether the hotkey was triggered by a key release (silences "press" and "up" events). bool retriggered; }; struct ReleasedHotkey { ReleasedHotkey(const char* n, bool t) : name(n), wasRetriggered(t) {}; const char* name; bool wasRetriggered; }; + // 'In-flight' state used because the hotkey triggering process is split in two phase. + // These hotkeys may still be stopped if the event responsible for triggering them is handled + // before it can be used to generate the hotkeys. + std::vector newPressedHotkeys; + // Stores the 'specificity' of the newly pressed hotkeys. + size_t closestMapMatch = 0; + // This is merely used to ensure consistency in EventWillFireHotkey. + const SDL_Event_* currentEvent; + // List of currently pressed hotkeys. This is used to quickly reset hotkeys. // This is an unsorted vector because there will generally be very few elements, // so it's presumably faster than std::set. std::vector pressedHotkeys; // List of active keys relevant for hotkeys. std::vector activeScancodes; } 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(CConfigDB& configDB) { for (const std::pair& configPair : configDB.GetValuesWithPrefix(CFG_COMMAND, "hotkey.")) { std::string hotkeyName = configPair.first.substr(7); // strip the "hotkey." prefix // "unused" is kept or the A23->24 migration, this can likely be removed in A25. if (configPair.second.empty() || (configPair.second.size() == 1 && configPair.second.front() == "unused")) { // Unused hotkeys must still be registered in the map to appear in the hotkey editor. SHotkeyMapping unusedCode; unusedCode.name = hotkeyName; unusedCode.primary = SKey{ UNUSED_HOTKEY_CODE }; 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 }; keyCombination.push_back(key); } std::vector::iterator itKey, itKey2; for (itKey = keyCombination.begin(); itKey != keyCombination.end(); ++itKey) { SHotkeyMapping bindCode; bindCode.name = hotkeyName; bindCode.primary = SKey{ itKey->code }; 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(CConfigDB& configDB) { pressedHotkeys.clear(); LoadConfigBindings(configDB); } void UnloadHotkeys() { pressedHotkeys.clear(); g_HotkeyMap.clear(); g_HotkeyStatus.clear(); } bool isPressed(const SKey& key) { // Normal keycodes are below EXTRA_KEYS_BASE 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) 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) return unified[key.code - UNIFIED_SHIFT]; // This codepath shouldn't be taken, but not having it triggers warnings. else return false; } InReaction HotkeyStateChange(const SDL_Event_* ev) { if (ev->ev.type == SDL_HOTKEYPRESS || ev->ev.type == SDL_HOTKEYPRESS_SILENT) g_HotkeyStatus[static_cast(ev->ev.user.data1)] = true; else if (ev->ev.type == SDL_HOTKEYUP || ev->ev.type == SDL_HOTKEYUP_SILENT) g_HotkeyStatus[static_cast(ev->ev.user.data1)] = false; return IN_PASS; } -InReaction HotkeyInputHandler(const SDL_Event_* ev) +InReaction HotkeyInputPrepHandler(const SDL_Event_* ev) { int scancode = SDL_SCANCODE_UNKNOWN; + // Restore default state. + newPressedHotkeys.clear(); + currentEvent = nullptr; + 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); + return HotkeyInputPrepHandler(&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); + return HotkeyInputPrepHandler(&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); + return HotkeyInputPrepHandler(&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); + return HotkeyInputPrepHandler(&phantom); } // Check whether we have any hotkeys registered that include this scancode. if (g_HotkeyMap.find(scancode) == g_HotkeyMap.end()) - return (IN_PASS); + return IN_PASS; + + currentEvent = ev; /** * Hotkey behaviour spec (see also tests): * - If both 'F' and 'Ctrl+F' are hotkeys, and Ctrl & F keys are down, then the more specific one only is fired ('Ctrl+F' here). * - If 'Ctrl+F' and 'Ctrl+A' are both hotkeys, both may fire simulatenously (respectively without Ctrl). * - However, per the first point, 'Ctrl+Shift+F' would fire alone in that situation. * - "Press" is sent once, when the hotkey is initially triggered. * - "Up" is sent once, when the hotkey is released or superseded by a more specific hotkey. * - "Down" is sent repeatedly, and is also sent alongside the inital "Press". * - As a special case (see below), "Down" is not sent alongside "PressSilent". * - If 'Ctrl+F' is active, and 'Ctrl' is released, 'F' must become active again. * - However, the "Press" event is _not_ fired. Instead, "PressSilent" is. * - Likewise, once 'F' is released, the "Up" event will be a "UpSilent". * (the reason is that it is unexpected to trigger a press on key release). * - Hotkeys are allowed to fire with extra keys (e.g. Ctrl+F+A still triggers 'Ctrl+F'). * - If 'F' and 'Ctrl+F' trigger the same hotkey, adding 'Ctrl' _and_ releasing 'Ctrl' will trigger new 'Press' events. * The "Up" event is only sent when both Ctrl & F are released. * - This is somewhat unexpected/buggy, but it makes the implementation easier and is easily avoidable for players. * - Wheel scrolling is 'instantaneous' behaviour and is essentially entirely separate from the above. * - It won't untrigger other hotkeys, and fires/releases on the same 'key event'. * Note that mouse buttons/wheel inputs can fire hotkeys, in combinations with keys. * ...Yes, this is all surprisingly complex. */ bool isReleasedKey = ev->ev.type == SDL_KEYUP || ev->ev.type == SDL_MOUSEBUTTONUP; // Wheel events are pressed & released in the same go. bool isInstantaneous = ev->ev.type == SDL_MOUSEWHEEL; if (!isInstantaneous) { std::vector::iterator it = std::find(activeScancodes.begin(), activeScancodes.end(), scancode); // This prevents duplicates, assuming we might end up in a weird state - feels safer with input. if (isReleasedKey && it != activeScancodes.end()) activeScancodes.erase(it); else if (!isReleasedKey && it == activeScancodes.end()) activeScancodes.emplace_back(scancode); } - std::vector releasedHotkeys; - std::vector newPressedHotkeys; - std::set triggers; if (!isReleasedKey || isInstantaneous) triggers.insert(scancode); else // If the key is released, we need to check all less precise hotkeys again, to see if we should retrigger some. for (SDL_Scancode_ code : activeScancodes) triggers.insert(code); // Now check if we need to trigger new hotkeys / retrigger hotkeys. // We'll need the match-level and the keys in play to release currently pressed hotkeys. - size_t closestMapMatch = 0; + closestMapMatch = 0; for (SDL_Scancode_ code : triggers) for (const SHotkeyMapping& hotkey : g_HotkeyMap[code]) { // Ensure no duplications in the new list. if (std::find_if(newPressedHotkeys.begin(), newPressedHotkeys.end(), [&hotkey](const PressedHotkey& v){ return v.mapping->name == hotkey.name; }) != newPressedHotkeys.end()) continue; bool accept = true; for (const SKey& k : hotkey.requires) { accept = isPressed(k); if (!accept) break; } if (!accept) continue; // 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 newPressedHotkeys.clear(); closestMapMatch = hotkey.requires.size() + 1; } newPressedHotkeys.emplace_back(&hotkey, isReleasedKey); } } + return IN_PASS; +} + +InReaction HotkeyInputActualHandler(const SDL_Event_* ev) +{ + if (!currentEvent) + return IN_PASS; + + bool isInstantaneous = ev->ev.type == SDL_MOUSEWHEEL; + + // TODO: it's probably possible to break hotkeys somewhat if the "Up" event that would release a hotkey is handled + // by a priori handler - it might be safer to do that in the 'Prep' phase. + std::vector releasedHotkeys; + // For instantaneous events, we don't update the pressedHotkeys (i.e. currently active hotkeys), // we just fire/release the triggered hotkeys transiently. // Therefore, skip the whole 'check pressedHotkeys & swap with newPressedHotkeys' logic. if (!isInstantaneous) { for (PressedHotkey& hotkey : pressedHotkeys) { bool addingAnew = std::find_if(newPressedHotkeys.begin(), newPressedHotkeys.end(), [&hotkey](const PressedHotkey& v){ return v.mapping->name == hotkey.mapping->name; }) != newPressedHotkeys.end(); // Update the triggered status to match our current state. if (addingAnew) std::find_if(newPressedHotkeys.begin(), newPressedHotkeys.end(), [&hotkey](const PressedHotkey& v){ return v.mapping->name == hotkey.mapping->name; })->retriggered = hotkey.retriggered; // If the already-pressed hotkey has a lower specificity than the new hotkey(s), de-activate it. else if (hotkey.mapping->requires.size() + 1 < closestMapMatch) { releasedHotkeys.emplace_back(hotkey.mapping->name.c_str(), hotkey.retriggered); continue; } // Check that the hotkey still matches all active keys. bool accept = isPressed(hotkey.mapping->primary); if (accept) for (const SKey& k : hotkey.mapping->requires) { accept = isPressed(k); if (!accept) break; } if (!accept && !addingAnew) releasedHotkeys.emplace_back(hotkey.mapping->name.c_str(), hotkey.retriggered); else if (accept) { // If this hotkey has higher specificity than the new hotkeys we wanted to trigger/retrigger, // then discard this new addition(s). This works because at any given time, all hotkeys // active must have the same specificity. if (hotkey.mapping->requires.size() + 1 > closestMapMatch) { closestMapMatch = hotkey.mapping->requires.size() + 1; newPressedHotkeys.clear(); newPressedHotkeys.emplace_back(hotkey.mapping, hotkey.retriggered); } else if (!addingAnew) newPressedHotkeys.emplace_back(hotkey.mapping, hotkey.retriggered); } } pressedHotkeys.swap(newPressedHotkeys); } for (const PressedHotkey& hotkey : isInstantaneous ? newPressedHotkeys : pressedHotkeys) { // 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 = hotkey.retriggered ? SDL_HOTKEYPRESS_SILENT : SDL_HOTKEYPRESS; hotkeyPressNotification.ev.user.data1 = const_cast(hotkey.mapping->name.c_str()); in_push_priority_event(&hotkeyPressNotification); } // Send a HotkeyDown event on every key, mouseButton and mouseWheel event. // The exception is on the first retriggering: hotkeys may fire transiently // while a user lifts fingers off multi-key hotkeys, and listeners to "hotkeydown" // generally don't expect that to trigger then. // (It might be better to check for HotkeyIsPressed, however). // 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. if (ev->ev.key.repeat == 0 && hotkey.retriggered) continue; SDL_Event_ hotkeyDownNotification; hotkeyDownNotification.ev.type = SDL_HOTKEYDOWN; hotkeyDownNotification.ev.user.data1 = const_cast(hotkey.mapping->name.c_str()); in_push_priority_event(&hotkeyDownNotification); } // Release instantaneous events (e.g. mouse wheel) right away. if (isInstantaneous) for (const PressedHotkey& hotkey : newPressedHotkeys) releasedHotkeys.emplace_back(hotkey.mapping->name.c_str(), false); for (const ReleasedHotkey& hotkey : releasedHotkeys) { SDL_Event_ hotkeyNotification; hotkeyNotification.ev.type = hotkey.wasRetriggered ? SDL_HOTKEYUP_SILENT : SDL_HOTKEYUP; hotkeyNotification.ev.user.data1 = const_cast(hotkey.name); in_push_priority_event(&hotkeyNotification); } return IN_PASS; } +bool EventWillFireHotkey(const SDL_Event_* ev, const CStr& keyname) +{ + // Sanity check of sort. This parameter mostly exists because it looks right from the caller's perspective. + if (ev != currentEvent || !currentEvent) + return false; + + return std::find_if(newPressedHotkeys.begin(), newPressedHotkeys.end(), + [&keyname](const PressedHotkey& v){ return v.mapping->name == keyname; }) != newPressedHotkeys.end(); +} + bool HotkeyIsPressed(const CStr& keyname) { return g_HotkeyStatus[keyname]; } Index: ps/trunk/source/ps/Hotkey.h =================================================================== --- ps/trunk/source/ps/Hotkey.h (revision 25179) +++ ps/trunk/source/ps/Hotkey.h (revision 25180) @@ -1,85 +1,105 @@ /* 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; const uint SDL_HOTKEYPRESS_SILENT = SDL_USEREVENT_ + 3; const uint SDL_HOTKEYUP_SILENT = SDL_USEREVENT_ + 4; constexpr SDL_Scancode_ UNUSED_HOTKEY_CODE = 0; // == SDL_SCANCODE_UNKNOWN struct SKey { SDL_Scancode_ code; // scancode or MOUSE_ or UNIFIED_ value 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 SKey primary; // the primary key 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; class CConfigDB; extern void LoadHotkeys(CConfigDB& configDB); extern void UnloadHotkeys(); +/** + * Updates g_HotkeyMap. + */ extern InReaction HotkeyStateChange(const SDL_Event_* ev); -extern InReaction HotkeyInputHandler(const SDL_Event_* ev); +/** + * Detects hotkeys that should fire. This allows using EventWillFireHotkey, + * (and then possibly preventing those hotkeys from firing by handling the event). + */ +extern InReaction HotkeyInputPrepHandler(const SDL_Event_* ev); +/** + * Actually fires hotkeys. + */ +extern InReaction HotkeyInputActualHandler(const SDL_Event_* ev); + +/** + * @return whether the event @param ev will fire the hotkey @param keyname. + */ +extern bool EventWillFireHotkey(const SDL_Event_* ev, const CStr& keyname); + +/** + * @return whether the hotkey is currently pressed (i.e. active). + */ extern bool HotkeyIsPressed(const CStr& keyname); #endif // INCLUDED_HOTKEY Index: ps/trunk/source/ps/tests/test_Hotkeys.h =================================================================== --- ps/trunk/source/ps/tests/test_Hotkeys.h (revision 25179) +++ ps/trunk/source/ps/tests/test_Hotkeys.h (revision 25180) @@ -1,317 +1,318 @@ /* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "lib/self_test.h" #include "lib/external_libraries/libsdl.h" #include "ps/Hotkey.h" #include "ps/ConfigDB.h" #include "ps/Globals.h" #include "ps/Filesystem.h" class TestHotkey : public CxxTest::TestSuite { CConfigDB* configDB; // Stores whether one of these was sent in the last fakeInput call. bool hotkeyPress = false; bool hotkeyUp = false; private: void fakeInput(const char* key, bool keyDown) { SDL_Event_ ev; ev.ev.type = keyDown ? SDL_KEYDOWN : SDL_KEYUP; ev.ev.key.repeat = 0; ev.ev.key.keysym.scancode = SDL_GetScancodeFromName(key); GlobalsInputHandler(&ev); - HotkeyInputHandler(&ev); + HotkeyInputPrepHandler(&ev); + HotkeyInputActualHandler(&ev); hotkeyPress = false; hotkeyUp = false; while(in_poll_priority_event(&ev)) { hotkeyUp |= ev.ev.type == SDL_HOTKEYUP; hotkeyPress |= ev.ev.type == SDL_HOTKEYPRESS; HotkeyStateChange(&ev); } } public: void setUp() { g_VFS = CreateVfs(); TS_ASSERT_OK(g_VFS->Mount(L"config", DataDir() / "_testconfig" / "")); TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "")); configDB = new CConfigDB; g_scancodes = {}; } void tearDown() { delete configDB; g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); DeleteDirectory(DataDir()/"_testconfig"); } void test_Hotkeys() { configDB->SetValueString(CFG_SYSTEM, "hotkey.A", "A"); configDB->SetValueString(CFG_SYSTEM, "hotkey.AB", "A+B"); configDB->SetValueString(CFG_SYSTEM, "hotkey.ABC", "A+B+C"); configDB->SetValueList(CFG_SYSTEM, "hotkey.D", { "D", "D+E" }); configDB->WriteFile(CFG_SYSTEM, "config/conf.cfg"); configDB->Reload(CFG_SYSTEM); UnloadHotkeys(); LoadHotkeys(*configDB); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); /** * Simple check. */ fakeInput("A", true); TS_ASSERT_EQUALS(hotkeyPress, true); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); fakeInput("A", false); TS_ASSERT_EQUALS(hotkeyUp, true); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); /** * Hotkey combinations: * - The most precise match only is selected * - Order does not matter. */ fakeInput("A", true); fakeInput("B", true); // HotkeyUp is true - A is released. TS_ASSERT_EQUALS(hotkeyUp, true); TS_ASSERT_EQUALS(hotkeyPress, true); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); fakeInput("B", false); // A is silently retriggered - no Press TS_ASSERT_EQUALS(hotkeyPress, false); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true); fakeInput("A", false); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); fakeInput("B", true); fakeInput("A", true); TS_ASSERT_EQUALS(hotkeyUp, false); TS_ASSERT_EQUALS(hotkeyPress, true); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); fakeInput("A", false); fakeInput("B", false); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); fakeInput("A", true); fakeInput("B", true); fakeInput("B", false); TS_ASSERT_EQUALS(hotkeyUp, true); // The "A" is retriggered silently. TS_ASSERT_EQUALS(hotkeyPress, false); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); fakeInput("A", false); fakeInput("D", true); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); } void test_double_combination() { configDB->SetValueString(CFG_SYSTEM, "hotkey.AB", "A+B"); configDB->SetValueList(CFG_SYSTEM, "hotkey.D", { "D", "E" }); configDB->WriteFile(CFG_SYSTEM, "config/conf.cfg"); configDB->Reload(CFG_SYSTEM); UnloadHotkeys(); LoadHotkeys(*configDB); // Bit of a special case > Both D and E trigger the same hotkey. // In that case, any key change that still gets the hotkey triggered // will re-trigger a "press", and on the final release, the "Up" is sent. fakeInput("D", true); TS_ASSERT_EQUALS(hotkeyUp, false); TS_ASSERT_EQUALS(hotkeyPress, true); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); fakeInput("E", true); TS_ASSERT_EQUALS(hotkeyUp, false); TS_ASSERT_EQUALS(hotkeyPress, true); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); fakeInput("D", false); TS_ASSERT_EQUALS(hotkeyUp, false); TS_ASSERT_EQUALS(hotkeyPress, true); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); fakeInput("E", false); TS_ASSERT_EQUALS(hotkeyUp, true); TS_ASSERT_EQUALS(hotkeyPress, false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); // Check that silent triggering works even in that case. fakeInput("D", true); fakeInput("E", true); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); fakeInput("A", true); fakeInput("B", true); TS_ASSERT_EQUALS(hotkeyUp, true); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); fakeInput("B", false); TS_ASSERT_EQUALS(hotkeyPress, false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); fakeInput("E", false); TS_ASSERT_EQUALS(hotkeyUp, false); TS_ASSERT_EQUALS(hotkeyPress, false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); // Note: as a consequence of the special case here - repressing E won't trigger a "press". fakeInput("E", true); TS_ASSERT_EQUALS(hotkeyUp, false); TS_ASSERT_EQUALS(hotkeyPress, false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); fakeInput("E", false); TS_ASSERT_EQUALS(hotkeyUp, false); TS_ASSERT_EQUALS(hotkeyPress, false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); fakeInput("D", false); TS_ASSERT_EQUALS(hotkeyUp, false); TS_ASSERT_EQUALS(hotkeyPress, false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); } void test_quirk() { configDB->SetValueString(CFG_SYSTEM, "hotkey.A", "A"); configDB->SetValueString(CFG_SYSTEM, "hotkey.AB", "A+B"); configDB->SetValueString(CFG_SYSTEM, "hotkey.ABC", "A+B+C"); configDB->SetValueList(CFG_SYSTEM, "hotkey.D", { "D", "D+E" }); configDB->SetValueString(CFG_SYSTEM, "hotkey.E", "E+C"); configDB->WriteFile(CFG_SYSTEM, "config/conf.cfg"); configDB->Reload(CFG_SYSTEM); UnloadHotkeys(); LoadHotkeys(*configDB); /** * Quirk of the implementation: hotkeys are allowed to fire with too many keys. * Further, hotkeys with the same # of keys are allowed to trigger at the same time. * This is required to make e.g. 'up' and 'left' scroll up-left when both are active. * It would be nice to extend this to 'non-conflicting hotkeys', but that's quickly far more complex. */ fakeInput("A", true); fakeInput("D", true); // A+D isn't a hotkey; both A and D are active. TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("E"), false); fakeInput("C", true); // A+D+C likewise. TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("E"), false); fakeInput("B", true); // A+B+C is a hotkey, more specific than A and D - both deactivated. TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("E"), false); fakeInput("E", true); // D+E is still less specific than A+B+C - nothing changes. TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("E"), false); fakeInput("B", false); // E & D activated - D+E and E+C have the same specificity. // The triggering is silent as it's from a key release. TS_ASSERT_EQUALS(hotkeyUp, true); TS_ASSERT_EQUALS(hotkeyPress, false); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("E"), true); fakeInput("E", false); // A and D again. TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("E"), false); fakeInput("A", false); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("E"), false); fakeInput("D", false); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("E"), false); UnloadHotkeys(); } };