Index: ps/trunk/binaries/data/mods/mod/gui/gui.rng =================================================================== --- ps/trunk/binaries/data/mods/mod/gui/gui.rng (revision 28134) +++ ps/trunk/binaries/data/mods/mod/gui/gui.rng (revision 28135) @@ -1,923 +1,928 @@ true false left center right top center bottom repeat mirrored_repeat clamp_to_edge -?\d*\.?\d+%?([\+\-]\d*\.?\d+%?)* 0 255 0 255 0 255 0 255 [A-Za-z]+ asc desc 0 1 1 + + + + + Index: ps/trunk/source/gui/ObjectTypes/CList.cpp =================================================================== --- ps/trunk/source/gui/ObjectTypes/CList.cpp (revision 28134) +++ ps/trunk/source/gui/ObjectTypes/CList.cpp (revision 28135) @@ -1,487 +1,489 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2024 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 "CList.h" #include "gui/CGUI.h" #include "gui/CGUIScrollBarVertical.h" #include "gui/SettingTypes/CGUIColor.h" #include "gui/SettingTypes/CGUIList.h" #include "lib/external_libraries/libsdl.h" #include "lib/timer.h" const CStr CList::EventNameSelectionChange = "SelectionChange"; const CStr CList::EventNameHoverChange = "HoverChange"; const CStr CList::EventNameMouseLeftClickItem = "MouseLeftClickItem"; const CStr CList::EventNameMouseLeftDoubleClickItem = "MouseLeftDoubleClickItem"; CList::CList(CGUI& pGUI) : IGUIObject(pGUI), IGUITextOwner(*static_cast(this)), IGUIScrollBarOwner(*static_cast(this)), m_Modified(false), m_PrevSelectedItem(-1), m_LastItemClickTime(0), m_BufferZone(this, "buffer_zone"), m_Font(this, "font"), m_ScrollBar(this, "scrollbar", false), m_ScrollBarStyle(this, "scrollbar_style"), m_ScrollBottom(this, "scroll_bottom", false), m_SoundDisabled(this, "sound_disabled"), m_SoundSelected(this, "sound_selected"), m_Sprite(this, "sprite"), m_SpriteOverlay(this, "sprite_overlay"), // Add sprite_disabled! TODO m_SpriteSelectArea(this, "sprite_selectarea"), m_SpriteSelectAreaOverlay(this, "sprite_selectarea_overlay"), m_TextColor(this, "textcolor"), m_TextColorSelected(this, "textcolor_selected"), m_Selected(this, "selected", -1), // Index selected. -1 is none. m_AutoScroll(this, "auto_scroll", false), m_Hovered(this, "hovered", -1), // Each list item has both a name (in 'list') and an associated data string (in 'list_data') m_List(this, "list"), m_ListData(this, "list_data") { // Add scroll-bar auto bar = std::make_unique(pGUI); bar->SetRightAligned(true); AddScrollBar(std::move(bar)); } CList::~CList() { } void CList::SetupText() { SetupText(false); } void CList::SetupText(bool append) { m_Modified = true; if (!append) // Delete all generated texts. // TODO: try to be cleverer if we want to update items before the end. m_GeneratedTexts.clear(); float width = GetListRect().GetWidth(); bool bottom = false; if (m_ScrollBar && GetScrollBar(0).GetStyle()) { if (m_ScrollBottom && GetScrollBar(0).GetPos() > GetScrollBar(0).GetMaxPos() - 1.5f) bottom = true; // remove scrollbar if applicable width -= GetScrollBar(0).GetStyle()->m_Width; } // Generate texts float buffered_y = 0.f; if (append && !m_ItemsYPositions.empty()) buffered_y = m_ItemsYPositions.back(); m_ItemsYPositions.resize(m_List->m_Items.size() + 1); for (size_t i = append ? m_List->m_Items.size() - 1 : 0; i < m_List->m_Items.size(); ++i) { CGUIText* text; if (!m_List->m_Items[i].GetOriginalString().empty()) text = &AddText(m_List->m_Items[i], m_Font, width, m_BufferZone); else { // Minimum height of a space character of the current font size CGUIString align_string; align_string.SetValue(L" "); text = &AddText(align_string, m_Font, width, m_BufferZone); } m_ItemsYPositions[i] = buffered_y; buffered_y += text->GetSize().Height; } m_ItemsYPositions[m_List->m_Items.size()] = buffered_y; // Setup scrollbar if (m_ScrollBar) { GetScrollBar(0).SetScrollRange(m_ItemsYPositions.back()); GetScrollBar(0).SetScrollSpace(GetListRect().GetHeight()); CRect rect = GetListRect(); GetScrollBar(0).SetX(rect.right); GetScrollBar(0).SetY(rect.top); GetScrollBar(0).SetZ(GetBufferedZ()); GetScrollBar(0).SetLength(rect.bottom - rect.top); if (bottom) GetScrollBar(0).SetPos(GetScrollBar(0).GetMaxPos()); } } void CList::ResetStates() { IGUIObject::ResetStates(); IGUIScrollBarOwner::ResetStates(); } void CList::UpdateCachedSize() { IGUIObject::UpdateCachedSize(); IGUITextOwner::UpdateCachedSize(); } void CList::HandleMessage(SGUIMessage& Message) { IGUIObject::HandleMessage(Message); IGUIScrollBarOwner::HandleMessage(Message); //IGUITextOwner::HandleMessage(Message); <== placed it after the switch instead! m_Modified = false; switch (Message.type) { case GUIM_SETTINGS_UPDATED: if (Message.value == "list") SetupText(); // If selected is changed, call "SelectionChange" if (Message.value == "selected") { // TODO: Check range if (m_AutoScroll) UpdateAutoScroll(); ScriptEvent(EventNameSelectionChange); } if (Message.value == "scrollbar") SetupText(); // Update scrollbar if (Message.value == "scrollbar_style") { GetScrollBar(0).SetScrollBarStyle(m_ScrollBarStyle); SetupText(); } break; case GUIM_MOUSE_PRESS_LEFT: { if (!m_Enabled) { PlaySound(m_SoundDisabled); break; } int hovered = GetHoveredItem(); if (hovered == -1) break; m_Selected.Set(hovered, true); UpdateAutoScroll(); PlaySound(m_SoundSelected); if (timer_Time() - m_LastItemClickTime < SELECT_DBLCLICK_RATE && hovered == m_PrevSelectedItem) this->SendMouseEvent(GUIM_MOUSE_DBLCLICK_LEFT_ITEM, EventNameMouseLeftDoubleClickItem); else this->SendMouseEvent(GUIM_MOUSE_PRESS_LEFT_ITEM, EventNameMouseLeftClickItem); m_LastItemClickTime = timer_Time(); m_PrevSelectedItem = hovered; break; } case GUIM_MOUSE_LEAVE: { if (m_Hovered == -1) break; m_Hovered.Set(-1, true); ScriptEvent(EventNameHoverChange); break; } case GUIM_MOUSE_OVER: { int hovered = GetHoveredItem(); if (hovered == m_Hovered) break; m_Hovered.Set(hovered, true); ScriptEvent(EventNameHoverChange); break; } case GUIM_LOAD: { GetScrollBar(0).SetScrollBarStyle(m_ScrollBarStyle); break; } default: break; } IGUITextOwner::HandleMessage(Message); } InReaction CList::ManuallyHandleKeys(const SDL_Event_* ev) { InReaction result = IN_PASS; if (ev->ev.type == SDL_KEYDOWN) { int szChar = ev->ev.key.keysym.sym; switch (szChar) { case SDLK_HOME: SelectFirstElement(); UpdateAutoScroll(); result = IN_HANDLED; break; case SDLK_END: SelectLastElement(); UpdateAutoScroll(); result = IN_HANDLED; break; case SDLK_UP: SelectPrevElement(); UpdateAutoScroll(); result = IN_HANDLED; break; case SDLK_DOWN: SelectNextElement(); UpdateAutoScroll(); result = IN_HANDLED; break; case SDLK_PAGEUP: GetScrollBar(0).ScrollMinusPlenty(); result = IN_HANDLED; break; case SDLK_PAGEDOWN: GetScrollBar(0).ScrollPlusPlenty(); result = IN_HANDLED; break; default: // Do nothing result = IN_PASS; } } return result; } void CList::Draw(CCanvas2D& canvas) { DrawList(canvas, m_Selected, m_Sprite, m_SpriteOverlay, m_SpriteSelectArea, m_SpriteSelectAreaOverlay, m_TextColor); } void CList::DrawList(CCanvas2D& canvas, const int& selected, const CGUISpriteInstance& sprite, const CGUISpriteInstance& spriteOverlay, const CGUISpriteInstance& spriteSelectArea, const CGUISpriteInstance& spriteSelectAreaOverlay, const CGUIColor& textColor) { CRect rect = GetListRect(); m_pGUI.DrawSprite(sprite, canvas, rect); float scroll = 0.f; if (m_ScrollBar) scroll = GetScrollBar(0).GetPos(); bool drawSelected = false; CRect rectSel; if (selected >= 0 && selected+1 < (int)m_ItemsYPositions.size()) { // Get rectangle of selection: rectSel = CRect( rect.left, rect.top + m_ItemsYPositions[selected] - scroll, rect.right, rect.top + m_ItemsYPositions[selected+1] - scroll); if (rectSel.top <= rect.bottom && rectSel.bottom >= rect.top) { if (rectSel.bottom > rect.bottom) rectSel.bottom = rect.bottom; if (rectSel.top < rect.top) rectSel.top = rect.top; if (m_ScrollBar) { // Remove any overlapping area of the scrollbar. if (rectSel.right > GetScrollBar(0).GetOuterRect().left && rectSel.right <= GetScrollBar(0).GetOuterRect().right) rectSel.right = GetScrollBar(0).GetOuterRect().left; if (rectSel.left >= GetScrollBar(0).GetOuterRect().left && rectSel.left < GetScrollBar(0).GetOuterRect().right) rectSel.left = GetScrollBar(0).GetOuterRect().right; } m_pGUI.DrawSprite(spriteSelectArea, canvas, rectSel); drawSelected = true; } } for (size_t i = 0; i < m_List->m_Items.size(); ++i) { if (m_ItemsYPositions[i+1] - scroll < 0 || m_ItemsYPositions[i] - scroll > rect.GetHeight()) continue; // Clipping area (we'll have to substract the scrollbar) CRect cliparea = GetListRect(); if (m_ScrollBar) { 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; } - DrawText(canvas, i, textColor, rect.TopLeft() - CVector2D(0.f, scroll - m_ItemsYPositions[i]), cliparea); + const CGUIColor& finalTextColor = (drawSelected && static_cast(selected) == i && *m_TextColorSelected) ? m_TextColorSelected : textColor; + + DrawText(canvas, i, finalTextColor, rect.TopLeft() - CVector2D(0.f, scroll - m_ItemsYPositions[i]), cliparea); } // Draw scrollbars on top of the content if (m_ScrollBar) IGUIScrollBarOwner::Draw(canvas); // Draw the overlays last m_pGUI.DrawSprite(spriteOverlay, canvas, rect); if (drawSelected) m_pGUI.DrawSprite(spriteSelectAreaOverlay, canvas, rectSel); } void CList::AddItem(const CGUIString& str, const CGUIString& data) { // Do not send a settings-changed message m_List.GetMutable().m_Items.push_back(str); m_ListData.GetMutable().m_Items.push_back(data); SetupText(true); } void CList::AddItem(const CGUIString& strAndData) { AddItem(strAndData, strAndData); } bool CList::HandleAdditionalChildren(const XMBData& xmb, const XMBElement& child) { int elmt_item = xmb.GetElementID("item"); if (child.GetNodeName() == elmt_item) { CGUIString vlist; vlist.SetValue(child.GetText().FromUTF8()); AddItem(vlist, vlist); return true; } return false; } void CList::SelectNextElement() { if (m_Selected != static_cast(m_List->m_Items.size()) - 1) { m_Selected.Set(m_Selected + 1, true); PlaySound(m_SoundSelected); } } void CList::SelectPrevElement() { if (m_Selected > 0) { m_Selected.Set(m_Selected - 1, true); PlaySound(m_SoundSelected); } } void CList::SelectFirstElement() { if (m_Selected >= 0) m_Selected.Set(0, true); } void CList::SelectLastElement() { const int index = static_cast(m_List->m_Items.size()) - 1; if (m_Selected != index) m_Selected.Set(index, true); } void CList::UpdateAutoScroll() { // No scrollbar, no scrolling (at least it's not made to work properly). if (!m_ScrollBar || m_Selected < 0 || static_cast(m_Selected) >= m_ItemsYPositions.size()) return; float scroll = GetScrollBar(0).GetPos(); // Check upper boundary if (m_ItemsYPositions[m_Selected] < scroll) { GetScrollBar(0).SetPos(m_ItemsYPositions[m_Selected]); return; // this means, if it wants to align both up and down at the same time // this will have precedence. } // Check lower boundary CRect rect = GetListRect(); if (m_ItemsYPositions[m_Selected+1]-rect.GetHeight() > scroll) GetScrollBar(0).SetPos(m_ItemsYPositions[m_Selected+1]-rect.GetHeight()); } int CList::GetHoveredItem() { const float scroll = m_ScrollBar ? GetScrollBar(0).GetPos() : 0.f; const CRect& rect = GetListRect(); CVector2D mouse = m_pGUI.GetMousePos(); mouse.Y += scroll; // Mouse is over scrollbar if (m_ScrollBar && GetScrollBar(0).IsVisible() && mouse.X >= GetScrollBar(0).GetOuterRect().left && mouse.X <= GetScrollBar(0).GetOuterRect().right) return -1; for (size_t i = 0; i < m_List->m_Items.size(); ++i) if (mouse.Y >= rect.top + m_ItemsYPositions[i] && mouse.Y < rect.top + m_ItemsYPositions[i + 1]) return i; return -1; } Index: ps/trunk/source/gui/ObjectTypes/COList.cpp =================================================================== --- ps/trunk/source/gui/ObjectTypes/COList.cpp (revision 28134) +++ ps/trunk/source/gui/ObjectTypes/COList.cpp (revision 28135) @@ -1,452 +1,459 @@ /* Copyright (C) 2024 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 "COList.h" #include "gui/CGUI.h" #include "gui/IGUIScrollBar.h" #include "gui/SettingTypes/CGUIColor.h" #include "gui/SettingTypes/CGUIList.h" #include "i18n/L10n.h" #include "ps/CLogger.h" const float SORT_SPRITE_DIM = 16.0f; const CVector2D COLUMN_SHIFT = CVector2D(0, 4); const CStr COList::EventNameSelectionColumnChange = "SelectionColumnChange"; COList::COList(CGUI& pGUI) : CList(pGUI), m_SpriteHeading(this, "sprite_heading"), m_Sortable(this, "sortable"), // The actual sorting is done in JS for more versatility m_SelectedColumn(this, "selected_column"), m_SelectedColumnOrder(this, "selected_column_order"), m_SpriteAsc(this, "sprite_asc"), // Show the order of sorting m_SpriteDesc(this, "sprite_desc"), m_SpriteNotSorted(this, "sprite_not_sorted") { } void COList::SetupText() { m_ItemsYPositions.resize(m_List->m_Items.size() + 1); // Delete all generated texts. Some could probably be saved, // but this is easier, and this function will never be called // continuously, or even often, so it'll probably be okay. m_GeneratedTexts.clear(); m_TotalAvailableColumnWidth = GetListRect().GetWidth(); // remove scrollbar if applicable if (m_ScrollBar && GetScrollBar(0).GetStyle()) m_TotalAvailableColumnWidth -= GetScrollBar(0).GetStyle()->m_Width; m_HeadingHeight = SORT_SPRITE_DIM; // At least the size of the sorting sprite for (const COListColumn& column : m_Columns) { float width = column.m_Width; if (column.m_Width > 0 && column.m_Width < 1) width *= m_TotalAvailableColumnWidth; CGUIString gui_string; gui_string.SetValue(column.m_Heading); const CGUIText& text = AddText(gui_string, m_Font, width, m_BufferZone); m_HeadingHeight = std::max(m_HeadingHeight, text.GetSize().Height + COLUMN_SHIFT.Y); } // Generate texts float buffered_y = 0.f; for (size_t i = 0; i < m_List->m_Items.size(); ++i) { m_ItemsYPositions[i] = buffered_y; float shift = 0.0f; for (const COListColumn& column : m_Columns) { float width = column.m_Width; if (column.m_Width > 0 && column.m_Width < 1) width *= m_TotalAvailableColumnWidth; CGUIText* text; if (!column.m_List->m_Items[i].GetOriginalString().empty()) text = &AddText(column.m_List->m_Items[i], m_Font, width, m_BufferZone); else { // Minimum height of a space character of the current font size CGUIString align_string; align_string.SetValue(L" "); text = &AddText(align_string, m_Font, width, m_BufferZone); } shift = std::max(shift, text->GetSize().Height); } buffered_y += shift; } m_ItemsYPositions[m_List->m_Items.size()] = buffered_y; if (m_ScrollBar) { CRect rect = GetListRect(); GetScrollBar(0).SetScrollRange(m_ItemsYPositions.back()); GetScrollBar(0).SetScrollSpace(rect.GetHeight()); GetScrollBar(0).SetX(rect.right); GetScrollBar(0).SetY(rect.top); GetScrollBar(0).SetZ(GetBufferedZ()); GetScrollBar(0).SetLength(rect.bottom - rect.top); } } CRect COList::GetListRect() const { return m_CachedActualSize + CRect(0, m_HeadingHeight, 0, 0); } void COList::HandleMessage(SGUIMessage& Message) { CList::HandleMessage(Message); switch (Message.type) { case GUIM_SETTINGS_UPDATED: { if (Message.value.find("heading_") == 0) SetupText(); break; } // If somebody clicks on the column heading case GUIM_MOUSE_PRESS_LEFT: { if (!m_Sortable) return; const CVector2D& mouse = m_pGUI.GetMousePos(); if (!m_CachedActualSize.PointInside(mouse)) return; float xpos = 0; for (const COListColumn& column : m_Columns) { if (column.m_Hidden) continue; float width = column.m_Width; // Check if it's a decimal value, and if so, assume relative positioning. if (column.m_Width < 1 && column.m_Width > 0) width *= m_TotalAvailableColumnWidth; CVector2D leftTopCorner = m_CachedActualSize.TopLeft() + CVector2D(xpos, 0); if (mouse.X >= leftTopCorner.X && mouse.X < leftTopCorner.X + width && mouse.Y < leftTopCorner.Y + m_HeadingHeight) { if (column.m_Id != static_cast(m_SelectedColumn)) { m_SelectedColumnOrder.Set(column.m_SortOrder, true); CStr selected_column = column.m_Id; m_SelectedColumn.Set(selected_column, true); } else m_SelectedColumnOrder.Set(-m_SelectedColumnOrder, true); ScriptEvent(EventNameSelectionColumnChange); PlaySound(m_SoundSelected); return; } xpos += width; } return; } default: return; } } bool COList::HandleAdditionalChildren(const XMBData& xmb, const XMBElement& child) { #define ELMT(x) int elmt_##x = xmb.GetElementID(#x) #define ATTR(x) int attr_##x = xmb.GetAttributeID(#x) ELMT(item); ELMT(column); ELMT(translatableAttribute); ATTR(id); ATTR(context); if (child.GetNodeName() == elmt_item) { CGUIString vlist; vlist.SetValue(child.GetText().FromUTF8()); AddItem(vlist, vlist); return true; } else if (child.GetNodeName() == elmt_column) { CStr id; XERO_ITER_ATTR(child, attr) { if (attr.Name == attr_id) id = attr.Value; } COListColumn column(this, id); for (XMBAttribute attr : child.GetAttributes()) { std::string_view attr_name(xmb.GetAttributeStringView(attr.Name)); CStr attr_value(attr.Value); if (attr_name == "textcolor") { if (!CGUI::ParseString(&m_pGUI, attr_value.FromUTF8(), column.m_TextColor)) LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name.data(), attr_value.c_str()); } + else if (attr_name == "textcolor_selected") + { + if (!CGUI::ParseString(&m_pGUI, attr_value.FromUTF8(), column.m_TextColorSelected)) + LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name.data(), attr_value.c_str()); + } else if (attr_name == "hidden") { bool hidden = false; if (!CGUI::ParseString(&m_pGUI, attr_value.FromUTF8(), hidden)) LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name.data(), attr_value.c_str()); else column.m_Hidden.Set(hidden, false); } else if (attr_name == "width") { float width; if (!CGUI::ParseString(&m_pGUI, attr_value.FromUTF8(), width)) LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name.data(), attr_value.c_str()); else { // Check if it's a relative value, and save as decimal if so. if (attr_value.find("%") != std::string::npos) width = width / 100.f; column.m_Width = width; } } else if (attr_name == "heading") { column.m_Heading.Set(attr_value.FromUTF8(), false); } else if (attr_name == "sort_order") { column.m_SortOrder.Set(attr_value == "desc" ? -1 : 1, false); } } for (XMBElement grandchild : child.GetChildNodes()) { if (grandchild.GetNodeName() != elmt_translatableAttribute) continue; CStr attributeName(grandchild.GetAttributes().GetNamedItem(attr_id)); // only the heading is translatable for list column if (attributeName.empty() || attributeName != "heading") { LOGERROR("GUI: translatable attribute in olist column that isn't a heading. (object: %s)", this->GetPresentableName().c_str()); continue; } CStr value(grandchild.GetText()); if (value.empty()) continue; CStr context(grandchild.GetAttributes().GetNamedItem(attr_context)); // Read the context if any. if (!context.empty()) { CStr translatedValue(g_L10n.TranslateWithContext(context, value)); column.m_Heading.Set(translatedValue.FromUTF8(), false); } else { CStr translatedValue(g_L10n.Translate(value)); column.m_Heading.Set(translatedValue.FromUTF8(), false); } } m_Columns.emplace_back(std::move(column)); return true; } return false; } void COList::AdditionalChildrenHandled() { SetupText(); } void COList::DrawList(CCanvas2D& canvas, const int& selected, const CGUISpriteInstance& sprite, const CGUISpriteInstance& spriteOverlay, const CGUISpriteInstance& spriteSelectArea, const CGUISpriteInstance& spriteSelectAreaOverlay, const CGUIColor& textColor) { CRect rect = GetListRect(); m_pGUI.DrawSprite(sprite, canvas, rect); float scroll = 0.f; if (m_ScrollBar) scroll = GetScrollBar(0).GetPos(); bool drawSelected = false; CRect rectSel; // Draw item selection if (selected != -1) { ENSURE(selected >= 0 && selected+1 < (int)m_ItemsYPositions.size()); // Get rectangle of selection: rectSel = CRect( rect.left, rect.top + m_ItemsYPositions[selected] - scroll, rect.right, rect.top + m_ItemsYPositions[selected+1] - scroll); if (rectSel.top <= rect.bottom && rectSel.bottom >= rect.top) { if (rectSel.bottom > rect.bottom) rectSel.bottom = rect.bottom; if (rectSel.top < rect.top) rectSel.top = rect.top; if (m_ScrollBar) { // Remove any overlapping area of the scrollbar. if (rectSel.right > GetScrollBar(0).GetOuterRect().left && rectSel.right <= GetScrollBar(0).GetOuterRect().right) rectSel.right = GetScrollBar(0).GetOuterRect().left; if (rectSel.left >= GetScrollBar(0).GetOuterRect().left && rectSel.left < GetScrollBar(0).GetOuterRect().right) rectSel.left = GetScrollBar(0).GetOuterRect().right; } // Draw item selection m_pGUI.DrawSprite(spriteSelectArea, canvas, rectSel); drawSelected = true; } } // Draw line above column header CRect rect_head(m_CachedActualSize.left, m_CachedActualSize.top, m_CachedActualSize.right, m_CachedActualSize.top + m_HeadingHeight); m_pGUI.DrawSprite(m_SpriteHeading, canvas, rect_head); // Draw column headers float xpos = 0; size_t col = 0; for (const COListColumn& column : m_Columns) { if (column.m_Hidden) { ++col; continue; } // Check if it's a decimal value, and if so, assume relative positioning. float width = column.m_Width; if (column.m_Width < 1 && column.m_Width > 0) width *= m_TotalAvailableColumnWidth; CVector2D leftTopCorner = m_CachedActualSize.TopLeft() + CVector2D(xpos, 0); // Draw sort arrows in colum header if (m_Sortable) { const CGUISpriteInstance* pSprite; if (*m_SelectedColumn == column.m_Id) { if (m_SelectedColumnOrder == 0) LOGERROR("selected_column_order must not be 0"); if (m_SelectedColumnOrder != -1) pSprite = &*m_SpriteAsc; else pSprite = &*m_SpriteDesc; } else pSprite = &*m_SpriteNotSorted; m_pGUI.DrawSprite(*pSprite, canvas, CRect(leftTopCorner + CVector2D(width - SORT_SPRITE_DIM, 0), leftTopCorner + CVector2D(width, SORT_SPRITE_DIM))); } // Draw column header text DrawText(canvas, col, textColor, leftTopCorner + COLUMN_SHIFT, rect_head); xpos += width; ++col; } // Draw list items for each column const size_t objectsCount = m_Columns.size(); for (size_t i = 0; i < m_List->m_Items.size(); ++i) { if (m_ItemsYPositions[i+1] - scroll < 0 || m_ItemsYPositions[i] - scroll > rect.GetHeight()) continue; const float rowHeight = m_ItemsYPositions[i+1] - m_ItemsYPositions[i]; // Clipping area (we'll have to substract the scrollbar) CRect cliparea = GetListRect(); if (m_ScrollBar) { 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; } // Draw all items for that column xpos = 0; for (size_t colIdx = 0; colIdx < m_Columns.size(); ++colIdx) { const COListColumn& column = m_Columns[colIdx]; if (column.m_Hidden) continue; // Determine text position and width const CVector2D textPos = rect.TopLeft() + CVector2D(xpos, -scroll + m_ItemsYPositions[i]); float width = column.m_Width; // Check if it's a decimal value, and if so, assume relative positioning. if (column.m_Width < 1 && column.m_Width > 0) width *= m_TotalAvailableColumnWidth; // Clip text to the column (to prevent drawing text into the neighboring column) CRect cliparea2 = cliparea; cliparea2.right = std::min(cliparea2.right, textPos.X + width); cliparea2.bottom = std::min(cliparea2.bottom, textPos.Y + rowHeight); + const CGUIColor& finalTextColor = (drawSelected && static_cast(selected) == i && column.m_TextColorSelected) ? column.m_TextColorSelected : column.m_TextColor; + // Draw list item - DrawText(canvas, objectsCount * (i +/*Heading*/1) + colIdx, column.m_TextColor, textPos, cliparea2); + DrawText(canvas, objectsCount * (i +/*Heading*/1) + colIdx, finalTextColor, textPos, cliparea2); xpos += width; } } // Draw scrollbars on top of the content if (m_ScrollBar) IGUIScrollBarOwner::Draw(canvas); // Draw the overlays last m_pGUI.DrawSprite(spriteOverlay, canvas, rect); if (drawSelected) m_pGUI.DrawSprite(spriteSelectAreaOverlay, canvas, rectSel); } Index: ps/trunk/source/gui/ObjectTypes/COList.h =================================================================== --- ps/trunk/source/gui/ObjectTypes/COList.h (revision 28134) +++ ps/trunk/source/gui/ObjectTypes/COList.h (revision 28135) @@ -1,101 +1,102 @@ -/* Copyright (C) 2023 Wildfire Games. +/* Copyright (C) 2024 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_COLIST #define INCLUDED_COLIST #include "gui/ObjectTypes/CList.h" #include "gui/SettingTypes/CGUIColor.h" #include /** * Represents a column. */ class COListColumn { public: COListColumn(IGUIObject* owner, const CStr& cid) : m_Id(cid), m_Width(0), m_Heading(owner, "heading_" + cid), m_List(owner, "list_" + cid), m_Hidden(owner, "hidden_" + cid, false), m_SortOrder(owner, " sort_order_" + cid, 1) {} // Avoid copying the strings. NONCOPYABLE(COListColumn); COListColumn(COListColumn&&) = default; COListColumn& operator=(COListColumn&&) = delete; CGUIColor m_TextColor; + CGUIColor m_TextColorSelected; CStr m_Id; float m_Width; CGUISimpleSetting m_Heading; // CGUIString?? CGUISimpleSetting m_List; CGUISimpleSetting m_Hidden; CGUISimpleSetting m_SortOrder; }; /** * Multi-column list. One row can be selected by the user. * Individual cells are clipped if the contained text is too long. * * The list can be sorted dynamically by JS code when a * heading is clicked. * A scroll-bar will appear when needed. */ class COList : public CList { GUI_OBJECT(COList) public: COList(CGUI& pGUI); protected: void SetupText(); void HandleMessage(SGUIMessage& Message); /** * Handle the \ tag. */ virtual bool HandleAdditionalChildren(const XMBData& xmb, const XMBElement& child); virtual void AdditionalChildrenHandled(); virtual void DrawList( CCanvas2D& canvas, const int& selected, const CGUISpriteInstance& sprite, const CGUISpriteInstance& spriteOverlay, const CGUISpriteInstance& spriteSelectarea, const CGUISpriteInstance& spriteSelectAreaOverlay, const CGUIColor& textColor); virtual CRect GetListRect() const; /** * Available columns. */ std::vector m_Columns; CGUISimpleSetting m_SpriteHeading; CGUISimpleSetting m_Sortable; CGUISimpleSetting m_SelectedColumn; CGUISimpleSetting m_SelectedColumnOrder; CGUISimpleSetting m_SpriteAsc; CGUISimpleSetting m_SpriteDesc; CGUISimpleSetting m_SpriteNotSorted; private: static const CStr EventNameSelectionColumnChange; // Width of space available for columns float m_TotalAvailableColumnWidth; float m_HeadingHeight; }; #endif // INCLUDED_COLIST