From 649755d0698a38afd2c6dbf966e56e37b380db47 Mon Sep 17 00:00:00 2001 From: Bruno Van de Velde Date: Sun, 27 Jan 2019 18:51:46 +0100 Subject: [PATCH] Items in ListView can now have icons --- include/TGUI/Widgets/ListView.hpp | 22 +++++ src/TGUI/Widgets/ListView.cpp | 131 ++++++++++++++++++++++++-- tests/Widgets/ListView.cpp | 45 ++++++--- tests/expected/ListView_Icons.png | Bin 0 -> 3850 bytes tests/expected/ListView_NoColumns.png | Bin 0 -> 2412 bytes 5 files changed, 178 insertions(+), 20 deletions(-) create mode 100644 tests/expected/ListView_Icons.png create mode 100644 tests/expected/ListView_NoColumns.png diff --git a/include/TGUI/Widgets/ListView.hpp b/include/TGUI/Widgets/ListView.hpp index 065a4546d..f9adb9859 100644 --- a/include/TGUI/Widgets/ListView.hpp +++ b/include/TGUI/Widgets/ListView.hpp @@ -60,6 +60,7 @@ namespace tgui struct Item { std::vector texts; + Sprite icon; }; struct Column @@ -292,6 +293,25 @@ namespace tgui int getSelectedItemIndex() const; + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Sets a small icon in front of the item + /// + /// @param index Index of the item + /// @param texture Texture of the item icon + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void setItemIcon(std::size_t index, const Texture& texture); + + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Gets the icon displayed in front of the item + /// + /// @param index Index of the item + /// + /// @return Texture of the item icon + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Texture getItemIcon(std::size_t index) const; + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Returns the amount of items in the list view /// @@ -661,6 +681,8 @@ namespace tgui unsigned int m_textSize = 0; unsigned int m_headerTextSize = 0; unsigned int m_separatorWidth = 1; + unsigned int m_iconCount = 0; + float m_maxIconWidth = 0; bool m_headerVisible = true; CopiedSharedPtr m_horizontalScrollbar; diff --git a/src/TGUI/Widgets/ListView.cpp b/src/TGUI/Widgets/ListView.cpp index 617fd6345..ef33826c2 100644 --- a/src/TGUI/Widgets/ListView.cpp +++ b/src/TGUI/Widgets/ListView.cpp @@ -269,6 +269,7 @@ namespace tgui { Item item; item.texts.push_back(createText(text)); + item.icon.setOpacity(m_opacityCached); m_items.push_back(std::move(item)); updateVerticalScrollbarMaximum(); @@ -289,6 +290,7 @@ namespace tgui for (const auto& text : itemTexts) item.texts.push_back(createText(text)); + item.icon.setOpacity(m_opacityCached); m_items.push_back(std::move(item)); updateVerticalScrollbarMaximum(); @@ -331,8 +333,30 @@ namespace tgui if (index >= m_items.size()) return false; + const bool wasIconSet = m_items[index].icon.isSet(); m_items.erase(m_items.begin() + index); + if (wasIconSet) + { + --m_iconCount; + + const float oldMaxIconWidth = m_maxIconWidth; + m_maxIconWidth = 0; + if (m_iconCount > 0) + { + // Rescan all items to find the largest icon + for (const auto& item : m_items) + { + if (!item.icon.isSet()) + continue; + + m_maxIconWidth = std::max(m_maxIconWidth, item.icon.getSize().x); + if (m_maxIconWidth == oldMaxIconWidth) + break; + } + } + } + updateVerticalScrollbarMaximum(); return true; } @@ -346,6 +370,9 @@ namespace tgui m_items.clear(); + m_iconCount = 0; + m_maxIconWidth = 0; + updateVerticalScrollbarMaximum(); } @@ -384,6 +411,60 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void ListView::setItemIcon(std::size_t index, const Texture& texture) + { + if (index >= m_items.size()) + { + TGUI_PRINT_WARNING("setItemIcon called with invalid index."); + return; + } + + const bool wasIconSet = m_items[index].icon.isSet(); + m_items[index].icon.setTexture(texture); + + if (m_items[index].icon.isSet()) + { + m_maxIconWidth = std::max(m_maxIconWidth, m_items[index].icon.getSize().x); + if (!wasIconSet) + ++m_iconCount; + } + else if (wasIconSet) + { + --m_iconCount; + + const float oldMaxIconWidth = m_maxIconWidth; + m_maxIconWidth = 0; + if (m_iconCount > 0) + { + // Rescan all items to find the largest icon + for (const auto& item : m_items) + { + if (!item.icon.isSet()) + continue; + + m_maxIconWidth = std::max(m_maxIconWidth, item.icon.getSize().x); + if (m_maxIconWidth == oldMaxIconWidth) + break; + } + } + } + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + Texture ListView::getItemIcon(std::size_t index) const + { + if (index < m_items.size()) + return m_items[index].icon.getTexture(); + else + { + TGUI_PRINT_WARNING("getItemIcon called with invalid index."); + return {}; + } + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + std::size_t ListView::getItemCount() const { return m_items.size(); @@ -914,6 +995,8 @@ namespace tgui { for (auto& text : item.texts) text.setOpacity(m_opacityCached); + + item.icon.setOpacity(m_opacityCached); } } else if (property == "font") @@ -1270,6 +1353,7 @@ namespace tgui void ListView::updateScrollbars() { + const bool verticalScrollbarAtBottom = (m_verticalScrollbar->getValue() + m_verticalScrollbar->getViewportSize() >= m_verticalScrollbar->getMaximum()); const float headerHeight = (m_headerVisible && !m_columns.empty()) ? getHeaderHeight() : 0.f; const Vector2f innerSize = {std::max(0.f, getInnerSize().x - m_paddingCached.getLeft() - m_paddingCached.getRight()), std::max(0.f, getInnerSize().y - m_paddingCached.getTop() - m_paddingCached.getBottom() - headerHeight)}; @@ -1295,6 +1379,10 @@ namespace tgui m_horizontalScrollbar->setSize({getInnerSize().x, m_horizontalScrollbar->getSize().y}); m_horizontalScrollbar->setViewportSize(static_cast(innerSize.x)); } + + // If the scrollbar was at the bottom then keep it at the bottom if it changes due to a different viewport size + if (verticalScrollbarAtBottom && (m_verticalScrollbar->getValue() + m_verticalScrollbar->getViewportSize() < m_verticalScrollbar->getMaximum())) + m_verticalScrollbar->setValue(m_verticalScrollbar->getMaximum() - m_verticalScrollbar->getViewportSize()); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1356,15 +1444,46 @@ namespace tgui if (firstItem == lastItem) return; + const float verticalTextOffset = (m_itemHeight - Text::getLineHeight(m_fontCached, m_textSize)) / 2.0f; const float headerHeight = (m_headerVisible && !m_columns.empty()) ? getHeaderHeight() : 0.f; const float textPadding = Text::getExtraHorizontalOffset(m_fontCached, m_textSize); const float columnHeight = getInnerSize().y - m_paddingCached.getTop() - m_paddingCached.getBottom() - headerHeight - (m_horizontalScrollbar->isShown() ? m_horizontalScrollbar->getSize().y : 0); - const Clipping clipping{target, states, {textPadding, 0}, {columnWidth - (2 * textPadding), columnHeight}}; - states.transform.translate({0, -static_cast(m_verticalScrollbar->getValue())}); - states.transform.translate({0, (m_itemHeight * firstItem) + (m_itemHeight - Text::getLineHeight(m_fontCached, m_textSize)) / 2.0f}); + // Draw the icons. + // If at least one icon is set then all items in the first column have to be shifted to make room for the icon. + if ((column == 0) && (m_iconCount > 0)) + { + const sf::Transform transformBeforeIcons = states.transform; + const Clipping clipping{target, states, {textPadding, 0}, {columnWidth - (2 * textPadding), columnHeight}}; + + states.transform.translate({0, (m_itemHeight * firstItem) - static_cast(m_verticalScrollbar->getValue())}); + + for (std::size_t i = firstItem; i < lastItem; ++i) + { + if (!m_items[i].icon.isSet()) + { + states.transform.translate({0, static_cast(m_itemHeight)}); + continue; + } + + const float verticalIconOffset = (m_itemHeight - m_items[i].icon.getSize().y) / 2.f; + + states.transform.translate({textPadding, verticalIconOffset}); + m_items[i].icon.draw(target, states); + states.transform.translate({-textPadding, static_cast(m_itemHeight) - verticalIconOffset}); + } + + states.transform = transformBeforeIcons; + + const float extraIconSpace = m_maxIconWidth + textPadding; + columnWidth -= extraIconSpace; + states.transform.translate({extraIconSpace, 0}); + } + + const Clipping clipping{target, states, {textPadding, 0}, {columnWidth - (2 * textPadding), columnHeight}}; + states.transform.translate({0, (m_itemHeight * firstItem) - static_cast(m_verticalScrollbar->getValue())}); for (std::size_t i = firstItem; i < lastItem; ++i) { if (column >= m_items[i].texts.size()) @@ -1374,16 +1493,16 @@ namespace tgui } float translateX; - if ((m_columns[column].alignment == ColumnAlignment::Left) || (column >= m_columns.size())) + if ((column >= m_columns.size()) || (m_columns[column].alignment == ColumnAlignment::Left)) translateX = textPadding; else if (m_columns[column].alignment == ColumnAlignment::Center) translateX = (columnWidth - m_items[i].texts[column].getSize().x) / 2.f; else // if (m_columns[column].alignment == ColumnAlignment::Right) translateX = columnWidth - textPadding - m_items[i].texts[column].getSize().x; - states.transform.translate({translateX, 0}); + states.transform.translate({translateX, verticalTextOffset}); m_items[i].texts[column].draw(target, states); - states.transform.translate({-translateX, static_cast(m_itemHeight)}); + states.transform.translate({-translateX, static_cast(m_itemHeight) - verticalTextOffset}); } } diff --git a/tests/Widgets/ListView.cpp b/tests/Widgets/ListView.cpp index 6a1a0c036..6b4ee3ca1 100644 --- a/tests/Widgets/ListView.cpp +++ b/tests/Widgets/ListView.cpp @@ -229,9 +229,14 @@ TEST_CASE("[ListView]") REQUIRE(listView->getTextSize() == 20); listView->setTextSize(0); - REQUIRE(listView->getTextSize() > 0); - REQUIRE(listView->getTextSize() != 20); - REQUIRE(listView->getTextSize() < 50); + const unsigned int textSize = listView->getTextSize(); + REQUIRE(textSize > 0); + REQUIRE(textSize != 20); + REQUIRE(textSize < 50); + + listView->setItemHeight(60); + REQUIRE(listView->getTextSize() > textSize); + REQUIRE(listView->getTextSize() < 60); } SECTION("HeaderTextSize") @@ -380,20 +385,20 @@ TEST_CASE("[ListView]") } } - SECTION("Header changes item positions") + SECTION("Click on header") { listView->setHeaderHeight(30); listView->addColumn("Col 1", 50); listView->addColumn("Col 2", 50); - mousePressed({40, 68}); - mouseReleased({40, 68}); - REQUIRE(listView->getSelectedItemIndex() == 0); + mousePressed({40, 35}); + mouseReleased({40, 35}); + REQUIRE(listView->getSelectedItemIndex() == -1); listView->setHeaderVisible(false); - mousePressed({40, 68}); - mouseReleased({40, 68}); - REQUIRE(listView->getSelectedItemIndex() == 2); + mousePressed({40, 35}); + mouseReleased({40, 35}); + REQUIRE(listView->getSelectedItemIndex() == 0); } SECTION("Vertical scrollbar interaction") @@ -623,10 +628,6 @@ TEST_CASE("[ListView]") renderer.setSelectedTextColorHover("#808080"); }; - listView->addColumn("C1", 40); - listView->addColumn("C2", 70); - listView->addColumn("C3", 70); - listView->addItem({"1", "1.2"}); listView->addItem("2"); listView->addItem({"3", "3.2"}); @@ -638,6 +639,15 @@ TEST_CASE("[ListView]") const sf::Vector2f mousePos2{30, 50}; const sf::Vector2f mousePos3{30, 45}; + SECTION("No columns") + { + TEST_DRAW("ListView_NoColumns.png") + } + + listView->addColumn("C1", 40); + listView->addColumn("C2", 70); + listView->addColumn("C3", 70); + SECTION("No selected item") { SECTION("No hover") @@ -782,5 +792,12 @@ TEST_CASE("[ListView]") } } } + + SECTION("Icons") + { + listView->setItemIcon(3, {"resources/Texture6.png", {0, 0, 20, 14}}); + listView->setItemIcon(4, {"resources/Texture7.png", {0, 0, 14, 14}}); + TEST_DRAW("ListView_Icons.png") + } } } diff --git a/tests/expected/ListView_Icons.png b/tests/expected/ListView_Icons.png new file mode 100644 index 0000000000000000000000000000000000000000..d6184870db5dba185d21768c9f7de5e2d064c1c3 GIT binary patch literal 3850 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 literal 0 HcmV?d00001 diff --git a/tests/expected/ListView_NoColumns.png b/tests/expected/ListView_NoColumns.png new file mode 100644 index 0000000000000000000000000000000000000000..4fa860454994a764bb2df1f7afa9fd59bcdda893 GIT binary patch literal 2412 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