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 : }
|