Line data Source code
1 : /* Copyright (C) 2023 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 "COList.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 "i18n/L10n.h"
27 : #include "ps/CLogger.h"
28 :
29 : const float SORT_SPRITE_DIM = 16.0f;
30 1 : const CVector2D COLUMN_SHIFT = CVector2D(0, 4);
31 :
32 1 : const CStr COList::EventNameSelectionColumnChange = "SelectionColumnChange";
33 :
34 0 : COList::COList(CGUI& pGUI)
35 : : CList(pGUI),
36 : m_SpriteHeading(this, "sprite_heading"),
37 : m_Sortable(this, "sortable"), // The actual sorting is done in JS for more versatility
38 : m_SelectedColumn(this, "selected_column"),
39 : m_SelectedColumnOrder(this, "selected_column_order"),
40 : m_SpriteAsc(this, "sprite_asc"), // Show the order of sorting
41 : m_SpriteDesc(this, "sprite_desc"),
42 0 : m_SpriteNotSorted(this, "sprite_not_sorted")
43 : {
44 0 : }
45 :
46 0 : void COList::SetupText()
47 : {
48 0 : m_ItemsYPositions.resize(m_List->m_Items.size() + 1);
49 :
50 : // Delete all generated texts. Some could probably be saved,
51 : // but this is easier, and this function will never be called
52 : // continuously, or even often, so it'll probably be okay.
53 0 : m_GeneratedTexts.clear();
54 :
55 0 : m_TotalAvailableColumnWidth = GetListRect().GetWidth();
56 : // remove scrollbar if applicable
57 0 : if (m_ScrollBar && GetScrollBar(0).GetStyle())
58 0 : m_TotalAvailableColumnWidth -= GetScrollBar(0).GetStyle()->m_Width;
59 :
60 0 : m_HeadingHeight = SORT_SPRITE_DIM; // At least the size of the sorting sprite
61 :
62 0 : for (const COListColumn& column : m_Columns)
63 : {
64 0 : float width = column.m_Width;
65 0 : if (column.m_Width > 0 && column.m_Width < 1)
66 0 : width *= m_TotalAvailableColumnWidth;
67 :
68 0 : CGUIString gui_string;
69 0 : gui_string.SetValue(column.m_Heading);
70 :
71 0 : const CGUIText& text = AddText(gui_string, m_Font, width, m_BufferZone);
72 0 : m_HeadingHeight = std::max(m_HeadingHeight, text.GetSize().Height + COLUMN_SHIFT.Y);
73 : }
74 :
75 : // Generate texts
76 0 : float buffered_y = 0.f;
77 :
78 0 : for (size_t i = 0; i < m_List->m_Items.size(); ++i)
79 : {
80 0 : m_ItemsYPositions[i] = buffered_y;
81 0 : float shift = 0.0f;
82 0 : for (const COListColumn& column : m_Columns)
83 : {
84 0 : float width = column.m_Width;
85 0 : if (column.m_Width > 0 && column.m_Width < 1)
86 0 : width *= m_TotalAvailableColumnWidth;
87 :
88 : CGUIText* text;
89 0 : if (!column.m_List->m_Items[i].GetOriginalString().empty())
90 0 : text = &AddText(column.m_List->m_Items[i], m_Font, width, m_BufferZone);
91 : else
92 : {
93 : // Minimum height of a space character of the current font size
94 0 : CGUIString align_string;
95 0 : align_string.SetValue(L" ");
96 0 : text = &AddText(align_string, m_Font, width, m_BufferZone);
97 : }
98 0 : shift = std::max(shift, text->GetSize().Height);
99 : }
100 0 : buffered_y += shift;
101 : }
102 :
103 0 : m_ItemsYPositions[m_List->m_Items.size()] = buffered_y;
104 :
105 0 : if (m_ScrollBar)
106 : {
107 0 : CRect rect = GetListRect();
108 0 : GetScrollBar(0).SetScrollRange(m_ItemsYPositions.back());
109 0 : GetScrollBar(0).SetScrollSpace(rect.GetHeight());
110 :
111 0 : GetScrollBar(0).SetX(rect.right);
112 0 : GetScrollBar(0).SetY(rect.top);
113 0 : GetScrollBar(0).SetZ(GetBufferedZ());
114 0 : GetScrollBar(0).SetLength(rect.bottom - rect.top);
115 : }
116 0 : }
117 :
118 0 : CRect COList::GetListRect() const
119 : {
120 0 : return m_CachedActualSize + CRect(0, m_HeadingHeight, 0, 0);
121 : }
122 :
123 0 : void COList::HandleMessage(SGUIMessage& Message)
124 : {
125 0 : CList::HandleMessage(Message);
126 :
127 0 : switch (Message.type)
128 : {
129 :
130 0 : case GUIM_SETTINGS_UPDATED:
131 : {
132 0 : if (Message.value.find("heading_") == 0)
133 0 : SetupText();
134 0 : break;
135 : }
136 :
137 : // If somebody clicks on the column heading
138 0 : case GUIM_MOUSE_PRESS_LEFT:
139 : {
140 0 : if (!m_Sortable)
141 0 : return;
142 :
143 0 : const CVector2D& mouse = m_pGUI.GetMousePos();
144 0 : if (!m_CachedActualSize.PointInside(mouse))
145 0 : return;
146 :
147 0 : float xpos = 0;
148 0 : for (const COListColumn& column : m_Columns)
149 : {
150 0 : if (column.m_Hidden)
151 0 : continue;
152 :
153 0 : float width = column.m_Width;
154 : // Check if it's a decimal value, and if so, assume relative positioning.
155 0 : if (column.m_Width < 1 && column.m_Width > 0)
156 0 : width *= m_TotalAvailableColumnWidth;
157 0 : CVector2D leftTopCorner = m_CachedActualSize.TopLeft() + CVector2D(xpos, 0);
158 0 : if (mouse.X >= leftTopCorner.X &&
159 0 : mouse.X < leftTopCorner.X + width &&
160 0 : mouse.Y < leftTopCorner.Y + m_HeadingHeight)
161 : {
162 0 : if (column.m_Id != static_cast<CStr>(m_SelectedColumn))
163 : {
164 0 : m_SelectedColumnOrder.Set(column.m_SortOrder, true);
165 0 : CStr selected_column = column.m_Id;
166 0 : m_SelectedColumn.Set(selected_column, true);
167 : }
168 : else
169 0 : m_SelectedColumnOrder.Set(-m_SelectedColumnOrder, true);
170 :
171 0 : ScriptEvent(EventNameSelectionColumnChange);
172 0 : PlaySound(m_SoundSelected);
173 0 : return;
174 : }
175 0 : xpos += width;
176 : }
177 0 : return;
178 : }
179 0 : default:
180 0 : return;
181 : }
182 : }
183 :
184 0 : bool COList::HandleAdditionalChildren(const XMBData& xmb, const XMBElement& child)
185 : {
186 : #define ELMT(x) int elmt_##x = xmb.GetElementID(#x)
187 : #define ATTR(x) int attr_##x = xmb.GetAttributeID(#x)
188 0 : ELMT(item);
189 0 : ELMT(column);
190 0 : ELMT(translatableAttribute);
191 0 : ATTR(id);
192 0 : ATTR(context);
193 :
194 0 : if (child.GetNodeName() == elmt_item)
195 : {
196 0 : CGUIString vlist;
197 0 : vlist.SetValue(child.GetText().FromUTF8());
198 0 : AddItem(vlist, vlist);
199 0 : return true;
200 : }
201 0 : else if (child.GetNodeName() == elmt_column)
202 : {
203 0 : CStr id;
204 0 : XERO_ITER_ATTR(child, attr)
205 : {
206 0 : if (attr.Name == attr_id)
207 0 : id = attr.Value;
208 : }
209 :
210 0 : COListColumn column(this, id);
211 :
212 0 : for (XMBAttribute attr : child.GetAttributes())
213 : {
214 0 : std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
215 0 : CStr attr_value(attr.Value);
216 :
217 0 : if (attr_name == "color")
218 : {
219 0 : if (!CGUI::ParseString<CGUIColor>(&m_pGUI, attr_value.FromUTF8(), column.m_TextColor))
220 0 : LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name.data(), attr_value.c_str());
221 : }
222 0 : else if (attr_name == "hidden")
223 : {
224 0 : bool hidden = false;
225 0 : if (!CGUI::ParseString<bool>(&m_pGUI, attr_value.FromUTF8(), hidden))
226 0 : LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name.data(), attr_value.c_str());
227 : else
228 0 : column.m_Hidden.Set(hidden, false);
229 : }
230 0 : else if (attr_name == "width")
231 : {
232 : float width;
233 0 : if (!CGUI::ParseString<float>(&m_pGUI, attr_value.FromUTF8(), width))
234 0 : LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name.data(), attr_value.c_str());
235 : else
236 : {
237 : // Check if it's a relative value, and save as decimal if so.
238 0 : if (attr_value.find("%") != std::string::npos)
239 0 : width = width / 100.f;
240 0 : column.m_Width = width;
241 : }
242 : }
243 0 : else if (attr_name == "heading")
244 : {
245 0 : column.m_Heading.Set(attr_value.FromUTF8(), false);
246 : }
247 0 : else if (attr_name == "sort_order")
248 : {
249 0 : column.m_SortOrder.Set(attr_value == "desc" ? -1 : 1, false);
250 : }
251 : }
252 :
253 0 : for (XMBElement grandchild : child.GetChildNodes())
254 : {
255 0 : if (grandchild.GetNodeName() != elmt_translatableAttribute)
256 0 : continue;
257 :
258 0 : CStr attributeName(grandchild.GetAttributes().GetNamedItem(attr_id));
259 : // only the heading is translatable for list column
260 0 : if (attributeName.empty() || attributeName != "heading")
261 : {
262 0 : LOGERROR("GUI: translatable attribute in olist column that isn't a heading. (object: %s)", this->GetPresentableName().c_str());
263 0 : continue;
264 : }
265 :
266 0 : CStr value(grandchild.GetText());
267 0 : if (value.empty())
268 0 : continue;
269 :
270 0 : CStr context(grandchild.GetAttributes().GetNamedItem(attr_context)); // Read the context if any.
271 0 : if (!context.empty())
272 : {
273 0 : CStr translatedValue(g_L10n.TranslateWithContext(context, value));
274 0 : column.m_Heading.Set(translatedValue.FromUTF8(), false);
275 : }
276 : else
277 : {
278 0 : CStr translatedValue(g_L10n.Translate(value));
279 0 : column.m_Heading.Set(translatedValue.FromUTF8(), false);
280 : }
281 : }
282 :
283 0 : m_Columns.emplace_back(std::move(column));
284 0 : return true;
285 : }
286 :
287 0 : return false;
288 : }
289 :
290 0 : void COList::AdditionalChildrenHandled()
291 : {
292 0 : SetupText();
293 0 : }
294 :
295 0 : void COList::DrawList(CCanvas2D& canvas, const int& selected, const CGUISpriteInstance& sprite, const CGUISpriteInstance& spriteOverlay,
296 : const CGUISpriteInstance& spriteSelectArea, const CGUISpriteInstance& spriteSelectAreaOverlay, const CGUIColor& textColor)
297 : {
298 0 : CRect rect = GetListRect();
299 :
300 0 : m_pGUI.DrawSprite(sprite, canvas, rect);
301 :
302 0 : float scroll = 0.f;
303 0 : if (m_ScrollBar)
304 0 : scroll = GetScrollBar(0).GetPos();
305 :
306 0 : bool drawSelected = false;
307 0 : CRect rectSel;
308 : // Draw item selection
309 0 : if (selected != -1)
310 : {
311 0 : ENSURE(selected >= 0 && selected+1 < (int)m_ItemsYPositions.size());
312 :
313 : // Get rectangle of selection:
314 0 : rectSel = CRect(
315 0 : rect.left, rect.top + m_ItemsYPositions[selected] - scroll,
316 0 : rect.right, rect.top + m_ItemsYPositions[selected+1] - scroll);
317 :
318 0 : if (rectSel.top <= rect.bottom &&
319 0 : rectSel.bottom >= rect.top)
320 : {
321 0 : if (rectSel.bottom > rect.bottom)
322 0 : rectSel.bottom = rect.bottom;
323 0 : if (rectSel.top < rect.top)
324 0 : rectSel.top = rect.top;
325 :
326 0 : if (m_ScrollBar)
327 : {
328 : // Remove any overlapping area of the scrollbar.
329 0 : if (rectSel.right > GetScrollBar(0).GetOuterRect().left &&
330 0 : rectSel.right <= GetScrollBar(0).GetOuterRect().right)
331 0 : rectSel.right = GetScrollBar(0).GetOuterRect().left;
332 :
333 0 : if (rectSel.left >= GetScrollBar(0).GetOuterRect().left &&
334 0 : rectSel.left < GetScrollBar(0).GetOuterRect().right)
335 0 : rectSel.left = GetScrollBar(0).GetOuterRect().right;
336 : }
337 :
338 : // Draw item selection
339 0 : m_pGUI.DrawSprite(spriteSelectArea, canvas, rectSel);
340 0 : drawSelected = true;
341 : }
342 : }
343 :
344 : // Draw line above column header
345 : CRect rect_head(m_CachedActualSize.left, m_CachedActualSize.top, m_CachedActualSize.right,
346 0 : m_CachedActualSize.top + m_HeadingHeight);
347 0 : m_pGUI.DrawSprite(m_SpriteHeading, canvas, rect_head);
348 :
349 : // Draw column headers
350 0 : float xpos = 0;
351 0 : size_t col = 0;
352 0 : for (const COListColumn& column : m_Columns)
353 : {
354 0 : if (column.m_Hidden)
355 : {
356 0 : ++col;
357 0 : continue;
358 : }
359 :
360 : // Check if it's a decimal value, and if so, assume relative positioning.
361 0 : float width = column.m_Width;
362 0 : if (column.m_Width < 1 && column.m_Width > 0)
363 0 : width *= m_TotalAvailableColumnWidth;
364 :
365 0 : CVector2D leftTopCorner = m_CachedActualSize.TopLeft() + CVector2D(xpos, 0);
366 :
367 : // Draw sort arrows in colum header
368 0 : if (m_Sortable)
369 : {
370 : const CGUISpriteInstance* pSprite;
371 0 : if (*m_SelectedColumn == column.m_Id)
372 : {
373 0 : if (m_SelectedColumnOrder == 0)
374 0 : LOGERROR("selected_column_order must not be 0");
375 :
376 0 : if (m_SelectedColumnOrder != -1)
377 0 : pSprite = &*m_SpriteAsc;
378 : else
379 0 : pSprite = &*m_SpriteDesc;
380 : }
381 : else
382 0 : pSprite = &*m_SpriteNotSorted;
383 :
384 0 : m_pGUI.DrawSprite(*pSprite, canvas, CRect(leftTopCorner + CVector2D(width - SORT_SPRITE_DIM, 0), leftTopCorner + CVector2D(width, SORT_SPRITE_DIM)));
385 : }
386 :
387 : // Draw column header text
388 0 : DrawText(canvas, col, textColor, leftTopCorner + COLUMN_SHIFT, rect_head);
389 0 : xpos += width;
390 0 : ++col;
391 : }
392 :
393 : // Draw list items for each column
394 0 : const size_t objectsCount = m_Columns.size();
395 0 : for (size_t i = 0; i < m_List->m_Items.size(); ++i)
396 : {
397 0 : if (m_ItemsYPositions[i+1] - scroll < 0 ||
398 0 : m_ItemsYPositions[i] - scroll > rect.GetHeight())
399 0 : continue;
400 :
401 0 : const float rowHeight = m_ItemsYPositions[i+1] - m_ItemsYPositions[i];
402 :
403 : // Clipping area (we'll have to substract the scrollbar)
404 0 : CRect cliparea = GetListRect();
405 :
406 0 : if (m_ScrollBar)
407 : {
408 0 : if (cliparea.right > GetScrollBar(0).GetOuterRect().left &&
409 0 : cliparea.right <= GetScrollBar(0).GetOuterRect().right)
410 0 : cliparea.right = GetScrollBar(0).GetOuterRect().left;
411 :
412 0 : if (cliparea.left >= GetScrollBar(0).GetOuterRect().left &&
413 0 : cliparea.left < GetScrollBar(0).GetOuterRect().right)
414 0 : cliparea.left = GetScrollBar(0).GetOuterRect().right;
415 : }
416 :
417 : // Draw all items for that column
418 0 : xpos = 0;
419 0 : for (size_t colIdx = 0; colIdx < m_Columns.size(); ++colIdx)
420 : {
421 0 : const COListColumn& column = m_Columns[colIdx];
422 0 : if (column.m_Hidden)
423 0 : continue;
424 :
425 : // Determine text position and width
426 0 : const CVector2D textPos = rect.TopLeft() + CVector2D(xpos, -scroll + m_ItemsYPositions[i]);
427 :
428 0 : float width = column.m_Width;
429 : // Check if it's a decimal value, and if so, assume relative positioning.
430 0 : if (column.m_Width < 1 && column.m_Width > 0)
431 0 : width *= m_TotalAvailableColumnWidth;
432 :
433 : // Clip text to the column (to prevent drawing text into the neighboring column)
434 0 : CRect cliparea2 = cliparea;
435 0 : cliparea2.right = std::min(cliparea2.right, textPos.X + width);
436 0 : cliparea2.bottom = std::min(cliparea2.bottom, textPos.Y + rowHeight);
437 :
438 : // Draw list item
439 0 : DrawText(canvas, objectsCount * (i +/*Heading*/1) + colIdx, column.m_TextColor, textPos, cliparea2);
440 0 : xpos += width;
441 : }
442 : }
443 :
444 : // Draw scrollbars on top of the content
445 0 : if (m_ScrollBar)
446 0 : IGUIScrollBarOwner::Draw(canvas);
447 :
448 : // Draw the overlays last
449 0 : m_pGUI.DrawSprite(spriteOverlay, canvas, rect);
450 0 : if (drawSelected)
451 0 : m_pGUI.DrawSprite(spriteSelectAreaOverlay, canvas, rectSel);
452 3 : }
|