LCOV - code coverage report
Current view: top level - source/gui/ObjectTypes - CDropDown.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 0 234 0.0 %
Date: 2023-01-19 00:18:29 Functions: 0 12 0.0 %

          Line data    Source code
       1             : /* Copyright (C) 2021 Wildfire Games.
       2             :  * This file is part of 0 A.D.
       3             :  *
       4             :  * 0 A.D. is free software: you can redistribute it and/or modify
       5             :  * it under the terms of the GNU General Public License as published by
       6             :  * the Free Software Foundation, either version 2 of the License, or
       7             :  * (at your option) any later version.
       8             :  *
       9             :  * 0 A.D. is distributed in the hope that it will be useful,
      10             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12             :  * GNU General Public License for more details.
      13             :  *
      14             :  * You should have received a copy of the GNU General Public License
      15             :  * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #include "precompiled.h"
      19             : 
      20             : #include "CDropDown.h"
      21             : 
      22             : #include "gui/CGUI.h"
      23             : #include "gui/IGUIScrollBar.h"
      24             : #include "gui/SettingTypes/CGUIColor.h"
      25             : #include "gui/SettingTypes/CGUIList.h"
      26             : #include "lib/external_libraries/libsdl.h"
      27             : #include "lib/timer.h"
      28             : #include "ps/Profile.h"
      29             : 
      30           0 : CDropDown::CDropDown(CGUI& pGUI)
      31             :     : CList(pGUI),
      32             :       m_Open(),
      33             :       m_HideScrollBar(),
      34             :       m_ElementHighlight(-1),
      35             :       m_ButtonWidth(this, "button_width"),
      36             :       m_DropDownSize(this, "dropdown_size"),
      37             :       m_DropDownBuffer(this, "dropdown_buffer"),
      38             :       m_MinimumVisibleItems(this, "minimum_visible_items"),
      39             :       m_SoundClosed(this, "sound_closed"),
      40             :       m_SoundEnter(this, "sound_enter"),
      41             :       m_SoundLeave(this, "sound_leave"),
      42             :       m_SoundOpened(this, "sound_opened"),
      43             :       // Setting "sprite" is registered by CList and used as the background
      44             :       m_SpriteDisabled(this, "sprite_disabled"),
      45             :       m_SpriteOverlayDisabled(this, "sprite_overlay_disabled"),
      46             :       m_SpriteList(this, "sprite_list"), // Background of the drop down list
      47             :       m_SpriteListOverlay(this, "sprite_list_overlay"), // Overlay above the drop down list
      48             :       m_Sprite2(this, "sprite2"), // Button that sits to the right
      49             :       m_Sprite2Over(this, "sprite2_over"),
      50             :       m_Sprite2Pressed(this, "sprite2_pressed"),
      51             :       m_Sprite2Disabled(this, "sprite2_disabled"),
      52           0 :       m_TextColorDisabled(this, "textcolor_disabled")
      53             :       // Add these in CList! And implement TODO
      54             :       //RegisterSetting("textcolor_over");
      55             :       //RegisterSetting("textcolor_pressed");
      56             : {
      57           0 :     m_ScrollBar.Set(true, true);
      58           0 : }
      59             : 
      60           0 : CDropDown::~CDropDown()
      61             : {
      62           0 : }
      63             : 
      64           0 : void CDropDown::SetupText()
      65             : {
      66           0 :     SetupListRect();
      67           0 :     CList::SetupText();
      68           0 : }
      69             : 
      70           0 : void CDropDown::UpdateCachedSize()
      71             : {
      72           0 :     CList::UpdateCachedSize();
      73           0 :     SetupText();
      74           0 : }
      75             : 
      76           0 : void CDropDown::HandleMessage(SGUIMessage& Message)
      77             : {
      78             :     // CList::HandleMessage(Message); placed after the switch!
      79             : 
      80           0 :     switch (Message.type)
      81             :     {
      82           0 :     case GUIM_SETTINGS_UPDATED:
      83             :     {
      84             :         // Update cached list rect
      85           0 :         if (Message.value == "size" ||
      86           0 :             Message.value == "absolute" ||
      87           0 :             Message.value == "dropdown_size" ||
      88           0 :             Message.value == "dropdown_buffer" ||
      89           0 :             Message.value == "minimum_visible_items" ||
      90           0 :             Message.value == "scrollbar_style" ||
      91           0 :             Message.value == "button_width")
      92             :         {
      93           0 :             SetupListRect();
      94             :         }
      95             : 
      96           0 :         break;
      97             :     }
      98             : 
      99           0 :     case GUIM_MOUSE_MOTION:
     100             :     {
     101           0 :         if (!m_Open)
     102           0 :             break;
     103             : 
     104           0 :         CVector2D mouse = m_pGUI.GetMousePos();
     105             : 
     106           0 :         if (!GetListRect().PointInside(mouse))
     107           0 :             break;
     108             : 
     109           0 :         const float scroll = m_ScrollBar ? GetScrollBar(0).GetPos() : 0.f;
     110             : 
     111           0 :         CRect rect = GetListRect();
     112           0 :         mouse.Y += scroll;
     113           0 :         int set = -1;
     114           0 :         for (int i = 0; i < static_cast<int>(m_List->m_Items.size()); ++i)
     115             :         {
     116           0 :             if (mouse.Y >= rect.top + m_ItemsYPositions[i] &&
     117           0 :                 mouse.Y < rect.top + m_ItemsYPositions[i+1] &&
     118             :                 // mouse is not over scroll-bar
     119           0 :                 (m_HideScrollBar ||
     120           0 :                     mouse.X < GetScrollBar(0).GetOuterRect().left ||
     121           0 :                     mouse.X > GetScrollBar(0).GetOuterRect().right))
     122             :             {
     123           0 :                 set = i;
     124             :             }
     125             :         }
     126             : 
     127           0 :         if (set != -1)
     128             :         {
     129           0 :             m_ElementHighlight = set;
     130             :             //UpdateAutoScroll();
     131             :         }
     132             : 
     133           0 :         break;
     134             :     }
     135             : 
     136           0 :     case GUIM_MOUSE_ENTER:
     137             :     {
     138           0 :         if (m_Enabled)
     139           0 :             PlaySound(m_SoundEnter);
     140           0 :         break;
     141             :     }
     142             : 
     143           0 :     case GUIM_MOUSE_LEAVE:
     144             :     {
     145           0 :         m_ElementHighlight = m_Selected;
     146             : 
     147           0 :         if (m_Enabled)
     148           0 :             PlaySound(m_SoundLeave);
     149           0 :         break;
     150             :     }
     151             : 
     152             :     // We can't inherent this routine from CList, because we need to include
     153             :     // a mouse click to open the dropdown, also the coordinates are changed.
     154           0 :     case GUIM_MOUSE_PRESS_LEFT:
     155             :     {
     156           0 :         if (!m_Enabled)
     157             :         {
     158           0 :             PlaySound(m_SoundDisabled);
     159           0 :             break;
     160             :         }
     161             : 
     162           0 :         if (!m_Open)
     163             :         {
     164           0 :             if (m_List->m_Items.empty())
     165           0 :                 return;
     166             : 
     167           0 :             m_Open = true;
     168           0 :             GetScrollBar(0).SetZ(GetBufferedZ());
     169           0 :             m_ElementHighlight = m_Selected;
     170             : 
     171             :             // Start at the position of the selected item, if possible.
     172           0 :             if (m_ItemsYPositions.empty() || m_ElementHighlight < 0 || static_cast<size_t>(m_ElementHighlight) >= m_ItemsYPositions.size())
     173           0 :                 GetScrollBar(0).SetPos(0);
     174             :             else
     175           0 :                 GetScrollBar(0).SetPos(m_ItemsYPositions[m_ElementHighlight] - 60);
     176             : 
     177           0 :             PlaySound(m_SoundOpened);
     178           0 :             return; // overshadow
     179             :         }
     180             :         else
     181             :         {
     182           0 :             const CVector2D& mouse = m_pGUI.GetMousePos();
     183             : 
     184             :             // If the regular area is pressed, then abort, and close.
     185           0 :             if (m_CachedActualSize.PointInside(mouse))
     186             :             {
     187           0 :                 m_Open = false;
     188           0 :                 GetScrollBar(0).SetZ(GetBufferedZ());
     189           0 :                 PlaySound(m_SoundClosed);
     190           0 :                 return; // overshadow
     191             :             }
     192             : 
     193           0 :             if (m_HideScrollBar ||
     194           0 :                 mouse.X < GetScrollBar(0).GetOuterRect().left ||
     195           0 :                 mouse.X > GetScrollBar(0).GetOuterRect().right ||
     196           0 :                 mouse.Y < GetListRect().top)
     197             :             {
     198           0 :                 m_Open = false;
     199           0 :                 GetScrollBar(0).SetZ(GetBufferedZ());
     200             :             }
     201             :         }
     202           0 :         break;
     203             :     }
     204             : 
     205           0 :     case GUIM_MOUSE_WHEEL_DOWN:
     206             :     {
     207             :         // Don't switch elements by scrolling when open, causes a confusing interaction between this and the scrollbar.
     208           0 :         if (m_Open || !m_Enabled)
     209           0 :             break;
     210             : 
     211           0 :         m_ElementHighlight = m_Selected;
     212             : 
     213           0 :         if (m_ElementHighlight + 1 >= (int)m_ItemsYPositions.size() - 1)
     214           0 :             break;
     215             : 
     216           0 :         ++m_ElementHighlight;
     217           0 :         m_Selected.Set(m_ElementHighlight, true);
     218           0 :         break;
     219             :     }
     220             : 
     221           0 :     case GUIM_MOUSE_WHEEL_UP:
     222             :     {
     223             :         // Don't switch elements by scrolling when open, causes a confusing interaction between this and the scrollbar.
     224           0 :         if (m_Open || !m_Enabled)
     225           0 :             break;
     226             : 
     227           0 :         m_ElementHighlight = m_Selected;
     228           0 :         if (m_ElementHighlight - 1 < 0)
     229           0 :             break;
     230             : 
     231           0 :         --m_ElementHighlight;
     232           0 :         m_Selected.Set(m_ElementHighlight, true);
     233           0 :         break;
     234             :     }
     235             : 
     236           0 :     case GUIM_LOST_FOCUS:
     237             :     {
     238           0 :         if (m_Open)
     239           0 :             PlaySound(m_SoundClosed);
     240             : 
     241           0 :         m_Open = false;
     242           0 :         break;
     243             :     }
     244             : 
     245           0 :     case GUIM_LOAD:
     246           0 :         SetupListRect();
     247           0 :         break;
     248             : 
     249           0 :     default:
     250           0 :         break;
     251             :     }
     252             : 
     253             :     // Important that this is after, so that overshadowed implementations aren't processed
     254           0 :     CList::HandleMessage(Message);
     255             : 
     256             :     // As HandleMessage functions return void, we need to manually verify
     257             :     // whether the child list's items were modified.
     258           0 :     if (CList::GetModified())
     259           0 :         SetupText();
     260             : }
     261             : 
     262           0 : InReaction CDropDown::ManuallyHandleKeys(const SDL_Event_* ev)
     263             : {
     264           0 :     InReaction result = IN_PASS;
     265           0 :     bool update_highlight = false;
     266             : 
     267           0 :     if (ev->ev.type == SDL_KEYDOWN)
     268             :     {
     269           0 :         int szChar = ev->ev.key.keysym.sym;
     270             : 
     271           0 :         switch (szChar)
     272             :         {
     273           0 :         case '\r':
     274           0 :             m_Open = false;
     275           0 :             result = IN_HANDLED;
     276           0 :             break;
     277             : 
     278           0 :         case SDLK_HOME:
     279             :         case SDLK_END:
     280             :         case SDLK_UP:
     281             :         case SDLK_DOWN:
     282             :         case SDLK_PAGEUP:
     283             :         case SDLK_PAGEDOWN:
     284           0 :             if (!m_Open)
     285           0 :                 return IN_PASS;
     286             :             // Set current selected item to highlighted, before
     287             :             //  then really processing these in CList::ManuallyHandleKeys()
     288           0 :             m_Selected.Set(m_ElementHighlight, true);
     289           0 :             update_highlight = true;
     290           0 :             break;
     291             : 
     292           0 :         default:
     293             :             // If we have typed a character try to get the closest element to it.
     294             :             // TODO: not too nice and doesn't deal with dashes.
     295           0 :             if (m_Open && ((szChar >= SDLK_a && szChar <= SDLK_z) || szChar == SDLK_SPACE
     296           0 :                            || (szChar >= SDLK_0 && szChar <= SDLK_9)
     297           0 :                            || (szChar >= SDLK_KP_1 && szChar <= SDLK_KP_0)))
     298             :             {
     299             :                 // arbitrary 1 second limit to add to string or start fresh.
     300             :                 // maximal amount of characters is 100, which imo is far more than enough.
     301           0 :                 if (timer_Time() - m_TimeOfLastInput > 1.0 || m_InputBuffer.length() >= 100)
     302           0 :                     m_InputBuffer = szChar;
     303             :                 else
     304           0 :                     m_InputBuffer += szChar;
     305             : 
     306           0 :                 m_TimeOfLastInput = timer_Time();
     307             : 
     308             :                 // let's look for the closest element
     309             :                 // basically it's alphabetic order and "as many letters as we can get".
     310           0 :                 int closest = -1;
     311           0 :                 int bestIndex = -1;
     312           0 :                 int difference = 1250;
     313           0 :                 for (int i = 0; i < static_cast<int>(m_List->m_Items.size()); ++i)
     314             :                 {
     315           0 :                     int indexOfDifference = 0;
     316           0 :                     int diff = 0;
     317           0 :                     for (size_t j = 0; j < m_InputBuffer.length(); ++j)
     318             :                     {
     319           0 :                         diff = std::abs(static_cast<int>(m_List->m_Items[i].GetRawString().LowerCase()[j]) - static_cast<int>(m_InputBuffer[j]));
     320           0 :                         if (diff == 0)
     321           0 :                             indexOfDifference = j+1;
     322             :                         else
     323           0 :                             break;
     324             :                     }
     325           0 :                     if (indexOfDifference > bestIndex || (indexOfDifference >= bestIndex && diff < difference))
     326             :                     {
     327           0 :                         bestIndex = indexOfDifference;
     328           0 :                         closest = i;
     329           0 :                         difference = diff;
     330             :                     }
     331             :                 }
     332             :                 // let's select the closest element. There should basically always be one.
     333           0 :                 if (closest != -1)
     334             :                 {
     335           0 :                     m_Selected.Set(closest, true);
     336           0 :                     update_highlight = true;
     337           0 :                     GetScrollBar(0).SetPos(m_ItemsYPositions[closest] - 60);
     338             :                 }
     339           0 :                 result = IN_HANDLED;
     340             :             }
     341           0 :             break;
     342             :         }
     343             :     }
     344             : 
     345           0 :     if (CList::ManuallyHandleKeys(ev) == IN_HANDLED)
     346           0 :         result = IN_HANDLED;
     347             : 
     348           0 :     if (update_highlight)
     349           0 :         m_ElementHighlight = m_Selected;
     350             : 
     351           0 :     return result;
     352             : }
     353             : 
     354           0 : void CDropDown::SetupListRect()
     355             : {
     356           0 :     const CSize2D windowSize = m_pGUI.GetWindowSize();
     357             : 
     358           0 :     if (m_ItemsYPositions.empty())
     359             :     {
     360           0 :         m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + m_DropDownBuffer,
     361           0 :                                  m_CachedActualSize.right, m_CachedActualSize.bottom + m_DropDownBuffer + m_DropDownSize);
     362           0 :         m_HideScrollBar = false;
     363             :     }
     364             :     // Too many items so use a scrollbar
     365           0 :     else if (m_ItemsYPositions.back() > m_DropDownSize)
     366             :     {
     367             :         // Place items below if at least some items can be placed below
     368           0 :         if (m_CachedActualSize.bottom + m_DropDownBuffer + m_DropDownSize <= windowSize.Height)
     369           0 :             m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + m_DropDownBuffer,
     370           0 :                                      m_CachedActualSize.right, m_CachedActualSize.bottom + m_DropDownBuffer + m_DropDownSize);
     371           0 :         else if ((m_ItemsYPositions.size() > m_MinimumVisibleItems && windowSize.Height - m_CachedActualSize.bottom - m_DropDownBuffer >= m_ItemsYPositions[m_MinimumVisibleItems]) ||
     372           0 :                  m_CachedActualSize.top < windowSize.Height - m_CachedActualSize.bottom)
     373           0 :             m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + m_DropDownBuffer,
     374           0 :                                      m_CachedActualSize.right, windowSize.Height);
     375             :         // Not enough space below, thus place items above
     376             :         else
     377           0 :             m_CachedListRect = CRect(m_CachedActualSize.left, std::max(0.f, m_CachedActualSize.top - m_DropDownBuffer - m_DropDownSize),
     378           0 :                                      m_CachedActualSize.right, m_CachedActualSize.top - m_DropDownBuffer);
     379             : 
     380           0 :         m_HideScrollBar = false;
     381             :     }
     382             :     else
     383             :     {
     384             :         // Enough space below, no scrollbar needed
     385           0 :         if (m_CachedActualSize.bottom + m_DropDownBuffer + m_ItemsYPositions.back() <= windowSize.Height)
     386             :         {
     387           0 :             m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + m_DropDownBuffer,
     388           0 :                                      m_CachedActualSize.right, m_CachedActualSize.bottom + m_DropDownBuffer + m_ItemsYPositions.back());
     389           0 :             m_HideScrollBar = true;
     390             :         }
     391             :         // Enough space below for some items, but not all, so place items below and use a scrollbar
     392           0 :         else if ((m_ItemsYPositions.size() > m_MinimumVisibleItems && windowSize.Height - m_CachedActualSize.bottom - m_DropDownBuffer >= m_ItemsYPositions[m_MinimumVisibleItems]) ||
     393           0 :                  m_CachedActualSize.top < windowSize.Height - m_CachedActualSize.bottom)
     394             :         {
     395           0 :             m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + m_DropDownBuffer,
     396           0 :                                      m_CachedActualSize.right, windowSize.Height);
     397           0 :             m_HideScrollBar = false;
     398             :         }
     399             :         // Not enough space below, thus place items above. Hide the scrollbar accordingly
     400             :         else
     401             :         {
     402           0 :             m_CachedListRect = CRect(m_CachedActualSize.left, std::max(0.f, m_CachedActualSize.top - m_DropDownBuffer - m_ItemsYPositions.back()),
     403           0 :                                      m_CachedActualSize.right, m_CachedActualSize.top - m_DropDownBuffer);
     404           0 :             m_HideScrollBar = m_CachedActualSize.top > m_ItemsYPositions.back() + m_DropDownBuffer;
     405             :         }
     406             :     }
     407           0 : }
     408             : 
     409           0 : CRect CDropDown::GetListRect() const
     410             : {
     411           0 :     return m_CachedListRect;
     412             : }
     413             : 
     414           0 : bool CDropDown::IsMouseOver() const
     415             : {
     416           0 :     if (m_Open)
     417             :     {
     418           0 :         CRect rect(m_CachedActualSize.left, std::min(m_CachedActualSize.top, GetListRect().top),
     419           0 :                    m_CachedActualSize.right, std::max(m_CachedActualSize.bottom, GetListRect().bottom));
     420           0 :         return rect.PointInside(m_pGUI.GetMousePos());
     421             :     }
     422             :     else
     423           0 :         return m_CachedActualSize.PointInside(m_pGUI.GetMousePos());
     424             : }
     425             : 
     426           0 : void CDropDown::Draw(CCanvas2D& canvas)
     427             : {
     428           0 :     const CGUISpriteInstance& sprite = m_Enabled ? m_Sprite : m_SpriteDisabled;
     429           0 :     const CGUISpriteInstance& spriteOverlay = m_Enabled ? m_SpriteOverlay : m_SpriteOverlayDisabled;
     430             : 
     431           0 :     m_pGUI.DrawSprite(sprite, canvas, m_CachedActualSize);
     432             : 
     433           0 :     if (m_ButtonWidth > 0.f)
     434             :     {
     435           0 :         CRect rect(m_CachedActualSize.right - m_ButtonWidth, m_CachedActualSize.top,
     436           0 :                    m_CachedActualSize.right, m_CachedActualSize.bottom);
     437             : 
     438           0 :         if (!m_Enabled)
     439             :         {
     440           0 :             m_pGUI.DrawSprite(*m_Sprite2Disabled ? m_Sprite2Disabled : m_Sprite2, canvas, rect);
     441             :         }
     442           0 :         else if (m_Open)
     443             :         {
     444           0 :             m_pGUI.DrawSprite(*m_Sprite2Pressed ? m_Sprite2Pressed : m_Sprite2, canvas, rect);
     445             :         }
     446           0 :         else if (m_MouseHovering)
     447             :         {
     448           0 :             m_pGUI.DrawSprite(*m_Sprite2Over ? m_Sprite2Over : m_Sprite2, canvas, rect);
     449             :         }
     450             :         else
     451           0 :             m_pGUI.DrawSprite(m_Sprite2, canvas, rect);
     452             :     }
     453             : 
     454           0 :     if (m_Selected != -1) // TODO: Maybe check validity completely?
     455             :     {
     456             :         CRect cliparea(m_CachedActualSize.left, m_CachedActualSize.top,
     457           0 :                        m_CachedActualSize.right - m_ButtonWidth, m_CachedActualSize.bottom);
     458             : 
     459           0 :         CVector2D pos(m_CachedActualSize.left, m_CachedActualSize.top);
     460           0 :         DrawText(canvas, m_Selected, m_Enabled ? m_TextColorSelected : m_TextColorDisabled, pos, cliparea);
     461             :     }
     462             : 
     463           0 :     if (m_Open)
     464             :     {
     465             :         // Disable scrollbar during drawing without sending a setting-changed message
     466           0 :         const bool old = m_ScrollBar;
     467             : 
     468             :         // TODO: drawScrollbar as an argument of DrawList?
     469           0 :         if (m_HideScrollBar)
     470           0 :             m_ScrollBar.Set(false, false);
     471             : 
     472           0 :         DrawList(canvas, m_ElementHighlight, m_SpriteList, m_SpriteListOverlay, m_SpriteSelectArea, m_SpriteSelectAreaOverlay, m_TextColor);
     473             : 
     474           0 :         if (m_HideScrollBar)
     475           0 :             m_ScrollBar.Set(old, false);
     476             :     }
     477           0 :     m_pGUI.DrawSprite(spriteOverlay, canvas, m_CachedActualSize);
     478           0 : }
     479             : 
     480             : // When a dropdown list is opened, it needs to be visible above all the other
     481             : // controls on the page. The only way I can think of to do this is to increase
     482             : // its z value when opened, so that it's probably on top.
     483           0 : float CDropDown::GetBufferedZ() const
     484             : {
     485           0 :     float bz = CList::GetBufferedZ();
     486           0 :     if (m_Open)
     487           0 :         return std::min(bz + 500.f, 1000.f); // TODO - don't use magic number for max z value
     488             :     else
     489           0 :         return bz;
     490             : }

Generated by: LCOV version 1.13