diff --git a/CMakeLists.txt b/CMakeLists.txt index 8936030c..f6bb8967 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,7 @@ set( "${INCLUDE_PATH}/SFGUI/Frame.hpp" "${INCLUDE_PATH}/SFGUI/Image.hpp" "${INCLUDE_PATH}/SFGUI/Label.hpp" + "${INCLUDE_PATH}/SFGUI/ListBox.hpp" "${INCLUDE_PATH}/SFGUI/Misc.hpp" "${INCLUDE_PATH}/SFGUI/Notebook.hpp" "${INCLUDE_PATH}/SFGUI/Object.hpp" @@ -115,6 +116,7 @@ set( "${SOURCE_PATH}/SFGUI/Engines/BREW/Frame.cpp" "${SOURCE_PATH}/SFGUI/Engines/BREW/Image.cpp" "${SOURCE_PATH}/SFGUI/Engines/BREW/Label.cpp" + "${SOURCE_PATH}/SFGUI/Engines/BREW/ListBox.cpp" "${SOURCE_PATH}/SFGUI/Engines/BREW/Notebook.cpp" "${SOURCE_PATH}/SFGUI/Engines/BREW/ProgressBar.cpp" "${SOURCE_PATH}/SFGUI/Engines/BREW/Scale.cpp" @@ -135,6 +137,7 @@ set( "${SOURCE_PATH}/SFGUI/GLLoader.hpp" "${SOURCE_PATH}/SFGUI/Image.cpp" "${SOURCE_PATH}/SFGUI/Label.cpp" + "${SOURCE_PATH}/SFGUI/ListBox.cpp" "${SOURCE_PATH}/SFGUI/Misc.cpp" "${SOURCE_PATH}/SFGUI/Notebook.cpp" "${SOURCE_PATH}/SFGUI/Object.cpp" @@ -267,7 +270,7 @@ elseif( APPLE ) elseif( "${CMAKE_SYSTEM_NAME}" MATCHES "Linux" ) target_link_libraries( sfgui ${SFML_LIBRARIES} ${SFML_DEPENDENCIES} ${OPENGL_gl_LIBRARY} ${X11_LIBRARIES} ) set( SHARE_PATH "${CMAKE_INSTALL_PREFIX}/share/SFGUI" ) - + if( LIB_SUFFIX ) set( LIB_PATH "lib${LIB_SUFFIX}" ) else() diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 0a81d307..e8f0d547 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -38,6 +38,7 @@ build_example( "ProgressBar" "ProgressBar.cpp" ) build_example( "SpinButton" "SpinButton.cpp" ) build_example( "Canvas" "Canvas.cpp" ) build_example( "CustomWidget" "CustomWidget.cpp" ) +build_example( "ListBox" "ListBox.cpp" ) build_example( "SFGUI-Test" "Test.cpp" ) # Copy data directory to build cache directory to be able to run examples from diff --git a/examples/ListBox.cpp b/examples/ListBox.cpp new file mode 100644 index 00000000..4dd46dda --- /dev/null +++ b/examples/ListBox.cpp @@ -0,0 +1,130 @@ +// Always include the necessary header files. +// Including SFGUI/Widgets.hpp includes everything +// you can possibly need automatically. +#include +#include + +#include + +int main() { + sfg::SFGUI sfgui; + sf::RenderWindow window(sf::VideoMode(800, 600), "ListBox Example"); + window.setVerticalSyncEnabled(true); + window.setFramerateLimit(30); + + sfg::Desktop desktop; + + auto window1 = sfg::Window::Create(); + window1->SetTitle( "ListBox with ItemTextPolicy::RESIZE_LISTBOX" ); + + auto box1 = sfg::Box::Create( sfg::Box::Orientation::VERTICAL ); + box1->SetSpacing( 5.f ); + box1->PackEnd( sfg::Label::Create( "The minimum width\nof this ListBox\ncorresponds to the largest\nitem's text width." ), false, true ); + + auto listbox1 = sfg::ListBox::Create(); + listbox1->AppendItem( "This is the first item" ); + listbox1->AppendItem( "Second item" ); + listbox1->AppendItem( "Third item which is a bit large" ); + listbox1->AppendItem( "Fourth item" ); + listbox1->AppendItem( "Fifth item" ); + listbox1->AppendItem( "Sixth item" ); + listbox1->AppendItem( "Last one !" ); + box1->PackEnd( listbox1 ); + + window1->Add( box1 ); + + auto window2 = sfg::Window::Create(); + window2->SetTitle( "ListBox with ItemTextPolicy::SHRINK" ); + + auto box2 = sfg::Box::Create( sfg::Box::Orientation::VERTICAL ); + box2->SetSpacing( 5.f ); + auto label2 = sfg::Label::Create( "The items' texts\nare shrinked if the\nListBox is not big\nenough." ); + box2->PackEnd( label2, false, true ); + + auto listbox2 = sfg::ListBox::Create(); + listbox2->AppendItem( "This is the first item (long text)" ); + listbox2->AppendItem( "Second item" ); + listbox2->AppendItem( "Third item which is very long !" ); + listbox2->AppendItem( "Fourth item" ); + listbox2->AppendItem( "Fifth item" ); + listbox2->AppendItem( "Sixth item, again it's too large !" ); + listbox2->AppendItem( "Last one !" ); + listbox2->SetItemTextPolicy( sfg::ListBox::ItemTextPolicy::SHRINK ); + box2->PackEnd( listbox2 ); + + window2->Add( box2 ); + + auto window3 = sfg::Window::Create(); + window3->SetTitle( "ListBox with ItemTextPolicy::SHRINK" ); + + auto box3 = sfg::Box::Create( sfg::Box::Orientation::VERTICAL ); + box3->SetSpacing( 5.f ); + auto label3 = sfg::Label::Create( "You can select multiple\nitems in this ListBox." ); + box3->PackEnd( label3, false, true ); + + auto listbox3 = sfg::ListBox::Create(); + listbox3->AppendItem( "First item" ); + listbox3->AppendItem( "Second item" ); + listbox3->AppendItem( "Third item" ); + listbox3->AppendItem( "Fourth item" ); + listbox3->AppendItem( "Fifth item" ); + listbox3->AppendItem( "Sixth item" ); + listbox3->AppendItem( "Last one !" ); + listbox3->SetSelectionMode( sfg::ListBox::SelectionMode::MULTI_SELECTION ); + listbox3->SetSelection( {1, 3, 4, 5} ); + box3->PackEnd( listbox3 ); + + window3->Add( box3 ); + + desktop.Add( window1 ); + desktop.Add( window2 ); + desktop.Add( window3 ); + + sf::Vector2f windowSize( static_cast( window.getSize().x ), static_cast( window.getSize().y ) ); + + window2->SetPosition(sf::Vector2f(windowSize.x/2.f - window2->GetRequisition().x/2.f, windowSize.y/2.f - window2->GetRequisition().y/2.f)); + window3->SetPosition(sf::Vector2f(windowSize.x - window3->GetRequisition().x - 100.f, windowSize.y - window3->GetRequisition().y - 100.f)); + + sf::Event event; + sf::Clock clock; + + window.resetGLStates(); + + int i = 0; + + while (window.isOpen()) + { + while (window.pollEvent(event)) + { + desktop.HandleEvent( event ); + switch(event.type) + { + case sf::Event::Closed: + window.close(); + break; + case sf::Event::KeyPressed: + if( event.key.code == sf::Keyboard::R ) { + listbox3->RemoveItem(2); + } else if( event.key.code == sf::Keyboard::I ) { + listbox3->InsertItem(3, "Inserted item #" + std::to_string(i)); + ++i; + } else if( event.key.code == sf::Keyboard::A) { + listbox3->AppendItem("Appended item #" + std::to_string(i)); + ++i; + } else if( event.key.code == sf::Keyboard::P) { + listbox3->PrependItem("Prepended item #" + std::to_string(i)); + ++i; + } + break; + default: + break; + } + } + desktop.Update( clock.restart().asSeconds() ); + window.clear(); + sfgui.Display( window ); + window.display(); + } + + return 0; +} diff --git a/include/SFGUI/Engine.hpp b/include/SFGUI/Engine.hpp index 00f5b64a..f8032859 100644 --- a/include/SFGUI/Engine.hpp +++ b/include/SFGUI/Engine.hpp @@ -40,6 +40,7 @@ class Notebook; class Spinner; class ComboBox; class SpinButton; +class ListBox; class Selector; class RenderQueue; @@ -164,6 +165,12 @@ class SFGUI_API Engine { */ virtual std::unique_ptr CreateSpinButtonDrawable( std::shared_ptr spinbutton ) const = 0; + /** Create drawable for listbox widgets. + * @param listbox Widget. + * @return New drawable object (unmanaged memory!). + */ + virtual std::unique_ptr CreateListBoxDrawable( std::shared_ptr listbox ) const = 0; + /** Get maximum line height. * @param font Font. * @param font_size Font size. diff --git a/include/SFGUI/Engines/BREW.hpp b/include/SFGUI/Engines/BREW.hpp index 34a88232..0d5ef5b2 100644 --- a/include/SFGUI/Engines/BREW.hpp +++ b/include/SFGUI/Engines/BREW.hpp @@ -43,6 +43,7 @@ class SFGUI_API BREW : public Engine { std::unique_ptr CreateSpinnerDrawable( std::shared_ptr spinner ) const override; std::unique_ptr CreateComboBoxDrawable( std::shared_ptr combo_box ) const override; std::unique_ptr CreateSpinButtonDrawable( std::shared_ptr spinbutton ) const override; + std::unique_ptr CreateListBoxDrawable( std::shared_ptr listbox ) const override; private: static std::unique_ptr CreateBorder( const sf::FloatRect& rect, float border_width, const sf::Color& light_color, const sf::Color& dark_color ); diff --git a/include/SFGUI/ListBox.hpp b/include/SFGUI/ListBox.hpp new file mode 100644 index 00000000..36b135ac --- /dev/null +++ b/include/SFGUI/ListBox.hpp @@ -0,0 +1,136 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include +#include + +namespace sfg { + +class SFGUI_API ListBox : public Container { + public: + typedef std::shared_ptr Ptr; //!< Shared pointer. + typedef std::shared_ptr PtrConst; //!< Shared pointer. + typedef int IndexType; + + static const IndexType NONE; + + enum class SelectionMode : char { + NO_SELECTION, + SINGLE_SELECTION, + MULTI_SELECTION, + DEFAULT = SINGLE_SELECTION + }; + + enum class ScrollbarPolicy : char { + VERTICAL_ALWAYS, + VERTICAL_AUTOMATIC, + VERTICAL_NEVER, + DEFAULT = VERTICAL_AUTOMATIC + }; + + enum class ItemTextPolicy : char { + RESIZE_LISTBOX, + SHRINK, + DEFAULT = RESIZE_LISTBOX + }; + + /** Create listbox. + * @return ListBox. + */ + static Ptr Create( ); + + const std::string& GetName() const override; + + void AppendItem( const sf::String& str ); + void InsertItem( IndexType index, const sf::String& str ); + void PrependItem( const sf::String& str ); + void ChangeItem( IndexType index, const sf::String& str ); + void RemoveItem( IndexType index ); + void Clear(); + + IndexType GetItemsCount() const; + const sf::String& GetItemText( IndexType index ) const; + const sf::String& GetDisplayedItemText( IndexType index ) const; + + IndexType GetHighlightedItem() const; + + void SetSelection( IndexType index ); + void SetSelection( std::initializer_list indices ); + void AppendToSelection( IndexType index ); + void RemoveFromSelection( IndexType index ); + void ClearSelection(); + + bool IsItemSelected( IndexType index ) const; + IndexType GetSelectedItemsCount() const; + IndexType GetSelectedItemIndex( IndexType index = 0 ) const; + const sf::String& GetSelectedItemText( IndexType index = 0 ) const; + + IndexType GetFirstDisplayedItemIndex() const; + IndexType GetDisplayedItemsCount() const; + IndexType GetMaxDisplayedItemsCount() const; + + SelectionMode GetSelectionMode() const; + void SetSelectionMode( SelectionMode mode ); + + ScrollbarPolicy GetScrollbarPolicy() const; + void SetScrollbarPolicy( ScrollbarPolicy policy ); + + ItemTextPolicy GetItemTextPolicy() const; + void SetItemTextPolicy( ItemTextPolicy policy ); + + // Signals. + static Signal::SignalID OnSelect; //!< Fired when an entry is selected. + + protected: + /** Ctor. + */ + ListBox(); + + std::unique_ptr InvalidateImpl() const override; + sf::Vector2f CalculateRequisition() override; + + private: + void HandleMouseEnter( int x, int y ) override; + void HandleMouseLeave( int x, int y ) override; + void HandleMouseMoveEvent( int x, int y ) override; + void HandleMouseButtonEvent( sf::Mouse::Button button, bool press, int x, int y ) override; + void HandleSizeChange() override; + bool HandleAdd( Widget::Ptr ) override; + void HandleRemove( Widget::Ptr ) override; + + IndexType GetItemAt( float y ) const; + + bool IsScrollbarVisible() const; + + void UpdateDisplayedItems(); + void UpdateScrollbarAdjustment(); + void UpdateScrollbarAllocation(); + + void UpdateDisplayedItemsText(); + + void OnScrollbarChanged(); + + std::vector m_items; + + SelectionMode m_selection_mode; + std::set m_selected_items; + + IndexType m_highlighted_item; + + IndexType m_first_displayed_item; + IndexType m_max_displayed_items_count; + + Scrollbar::Ptr m_vertical_scrollbar; + ScrollbarPolicy m_scrollbar_policy; + + ItemTextPolicy m_item_text_policy; + std::vector m_displayed_items_texts; +}; + +} diff --git a/include/SFGUI/Widgets.hpp b/include/SFGUI/Widgets.hpp index 45774997..ebc07f25 100644 --- a/include/SFGUI/Widgets.hpp +++ b/include/SFGUI/Widgets.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include diff --git a/src/SFGUI/Engines/BREW.cpp b/src/SFGUI/Engines/BREW.cpp index a221c1ed..3838b755 100644 --- a/src/SFGUI/Engines/BREW.cpp +++ b/src/SFGUI/Engines/BREW.cpp @@ -153,6 +153,12 @@ void BREW::ResetProperties() { SetProperty( "SpinButton", "StepperSpeed", 10.f ); SetProperty( "SpinButton", "StepperRepeatDelay", 500 ); + // ListBox-specific. + SetProperty( "ListBox", "BackgroundColor", sf::Color( 0x5e, 0x5e, 0x5e ) ); + SetProperty( "ListBox", "Color", sf::Color::White ); + SetProperty( "ListBox", "HighlightedColor", sf::Color( 0x65, 0x67, 0x62 ) ); + SetProperty( "ListBox", "SelectedColor", sf::Color( 0x5a, 0x6a, 0x50 ) ); + // (Re)Enable automatic widget refreshing after we are done setting all these properties. SetAutoRefresh( true ); } diff --git a/src/SFGUI/Engines/BREW/ListBox.cpp b/src/SFGUI/Engines/BREW/ListBox.cpp new file mode 100644 index 00000000..c6753001 --- /dev/null +++ b/src/SFGUI/Engines/BREW/ListBox.cpp @@ -0,0 +1,89 @@ +#include +#include +#include +#include + +#include + +namespace sfg { +namespace eng { + +std::unique_ptr BREW::CreateListBoxDrawable( std::shared_ptr listbox ) const { + auto border_color = GetProperty( "BorderColor", listbox ); + auto background_color = GetProperty( "BackgroundColor", listbox ); + auto highlighted_color = GetProperty( "HighlightedColor", listbox ); + auto selected_color = GetProperty( "SelectedColor", listbox ); + auto text_color = GetProperty( "Color", listbox ); + auto text_padding = GetProperty( "Padding", listbox ); + auto border_width = GetProperty( "BorderWidth", listbox ); + auto border_color_shift = GetProperty( "BorderColorShift", listbox ); + const auto& font_name = GetProperty( "FontName", listbox ); + const auto& font = GetResourceManager().GetFont( font_name ); + auto font_size = GetProperty( "FontSize", listbox ); + + std::unique_ptr queue( new RenderQueue ); + + // Pane. + queue->Add( + Renderer::Get().CreatePane( + sf::Vector2f( 0.f, 0.f ), + sf::Vector2f( listbox->GetAllocation().width, listbox->GetAllocation().height ), + border_width, + background_color, + border_color, + -border_color_shift + ) + ); + + // Items. + sf::Vector2f itemPosition = sf::Vector2f( border_width + text_padding, border_width + text_padding ); + for( ListBox::IndexType i = listbox->GetFirstDisplayedItemIndex(); + i < std::min(listbox->GetFirstDisplayedItemIndex() + listbox->GetMaxDisplayedItemsCount(), listbox->GetItemsCount()); + ++i ) { + auto& itemText = listbox->GetDisplayedItemText( i ); + + auto metrics = GetTextStringMetrics( itemText, *font, font_size ); + metrics.y = GetFontLineHeight( *font, font_size ); + + sf::Text text( itemText, *font, font_size ); + text.setPosition(itemPosition); + text.setColor(text_color); + + if( listbox->IsItemSelected( i ) ) { + queue->Add( + Renderer::Get().CreateRect( + sf::FloatRect( + itemPosition.x - text_padding, + itemPosition.y - text_padding, + listbox->GetAllocation().width - 2 * border_width, + std::min(metrics.y + text_padding * 2, listbox->GetAllocation().height - (itemPosition.y - text_padding) - border_width) + // Avoid the selection box to go further than the widget's height when the last displayed item padding space is not fully displayed. + ), + selected_color + ) + ); + } else if( i == listbox->GetHighlightedItem() ) { + queue->Add( + Renderer::Get().CreateRect( + sf::FloatRect( + itemPosition.x - text_padding, + itemPosition.y - text_padding, + listbox->GetAllocation().width - 2 * border_width, + std::min(metrics.y + text_padding * 2, listbox->GetAllocation().height - (itemPosition.y - text_padding) - border_width) + // Avoid the highlight box to go further than the widget's height when the last displayed item padding space is not fully displayed. + ), + highlighted_color + ) + ); + } + + queue->Add( Renderer::Get().CreateText(text) ); + + itemPosition += sf::Vector2f( 0.f, metrics.y + text_padding * 2 ); + } + + return queue; +} + +} +} diff --git a/src/SFGUI/ListBox.cpp b/src/SFGUI/ListBox.cpp new file mode 100644 index 00000000..c4a52ea9 --- /dev/null +++ b/src/SFGUI/ListBox.cpp @@ -0,0 +1,565 @@ +#include +#include +#include +#include +#include + +#include + +#include + +namespace sfg { + +const ListBox::IndexType ListBox::NONE = -1; +static const sf::String EMPTY = ""; + +Signal::SignalID ListBox::OnSelect = 0; + +ListBox::Ptr ListBox::Create( ) { + auto ptr = Ptr( new ListBox ); + static_cast( ptr )->Add( ptr->m_vertical_scrollbar ); + return ptr; +} + +ListBox::ListBox() : + Container(), + m_items(), + m_selection_mode(SelectionMode::DEFAULT), + m_selected_items(), + m_highlighted_item(NONE), + m_first_displayed_item(0), + m_max_displayed_items_count(0), + m_vertical_scrollbar(nullptr), + m_scrollbar_policy(ScrollbarPolicy::DEFAULT), + m_item_text_policy(ItemTextPolicy::DEFAULT), + m_displayed_items_texts() +{ + m_vertical_scrollbar = Scrollbar::Create(Scrollbar::Orientation::VERTICAL); + m_vertical_scrollbar->GetAdjustment()->GetSignal(sfg::Adjustment::OnChange).Connect(std::bind(&ListBox::OnScrollbarChanged, this)); +} + +std::unique_ptr ListBox::InvalidateImpl() const { + return Context::Get().GetEngine().CreateListBoxDrawable( std::dynamic_pointer_cast( shared_from_this() ) ); +} + +sf::Vector2f ListBox::CalculateRequisition() { + auto text_padding = Context::Get().GetEngine().GetProperty( "Padding", shared_from_this() ); + auto border_width = Context::Get().GetEngine().GetProperty( "BorderWidth", shared_from_this() ); + const auto& font_name = Context::Get().GetEngine().GetProperty( "FontName", shared_from_this() ); + const auto& font = Context::Get().GetEngine().GetResourceManager().GetFont( font_name ); + auto font_size = Context::Get().GetEngine().GetProperty( "FontSize", shared_from_this() ); + auto dots_width = Context::Get().GetEngine().GetTextStringMetrics("...", *font, font_size).x; + + // Calculate the max width of items + float items_max_width = 0.f; + for( const sf::String& item : m_items ) { + items_max_width = std::max(items_max_width, Context::Get().GetEngine().GetTextStringMetrics(item, *font, font_size).x); + } + + return sf::Vector2f( + border_width * 2 + text_padding * 2 + + ( m_item_text_policy == ItemTextPolicy::RESIZE_LISTBOX ? items_max_width : dots_width ) + + ( IsScrollbarVisible() ? m_vertical_scrollbar->GetRequisition().x : 0 ), + std::max(m_vertical_scrollbar->GetRequisition().y, 50.f) + ); +} + +const std::string& ListBox::GetName() const { + static const std::string name( "ListBox" ); + return name; +} + +void ListBox::AppendItem( const sf::String& str ) { + m_items.push_back( str ); + + UpdateDisplayedItems(); + RequestResize(); + HandleSizeChange(); + Invalidate(); +} + +void ListBox::InsertItem( IndexType index, const sf::String& str ) { + m_items.insert( m_items.begin() + index, str ); + + // Update next selected indexes (decrement them). + std::set new_selected_items; + std::transform( + m_selected_items.cbegin(), + m_selected_items.cend(), + std::inserter(new_selected_items, new_selected_items.end()), + [&](IndexType i){ + if( i < index ) + return i; + else + return ++i; + } + ); + m_selected_items = std::move(new_selected_items); + + UpdateDisplayedItems(); + RequestResize(); + HandleSizeChange(); + Invalidate(); +} + +void ListBox::PrependItem( const sf::String& str ) { + m_items.insert( m_items.begin(), str ); + + // Update selected items indexes. + std::set new_selected_items; + std::transform( + m_selected_items.cbegin(), + m_selected_items.cend(), + std::inserter(new_selected_items, new_selected_items.end()), + [&](IndexType i){ + return ++i; + } + ); + m_selected_items = std::move(new_selected_items); + + UpdateDisplayedItems(); + RequestResize(); + HandleSizeChange(); + Invalidate(); +} + +void ListBox::ChangeItem( IndexType index, const sf::String& str ) { + if( index >= static_cast( m_items.size() ) || index < 0 ) { + return; + } + + m_items[ static_cast( index ) ] = str; + + UpdateDisplayedItems(); + Invalidate(); +} + +void ListBox::RemoveItem( IndexType index ) { + if( index >= static_cast( m_items.size() ) || index < 0 ) { + return; + } + + m_items.erase( m_items.begin() + index ); + + // Remove it from the selected indexes. + m_selected_items.erase( index ); + + // Update next selected indexes (decrement them). + std::set new_selected_items; + std::transform( + m_selected_items.cbegin(), + m_selected_items.cend(), + std::inserter(new_selected_items, new_selected_items.end()), + [&](IndexType i){ + if( i > index ) + return --i; + else + return i; + } + ); + m_selected_items = std::move(new_selected_items); + + UpdateDisplayedItems(); + RequestResize(); + HandleSizeChange(); + Invalidate(); +} + +void ListBox::Clear() { + m_items.clear(); + m_selected_items.clear(); + + UpdateDisplayedItems(); + RequestResize(); + HandleSizeChange(); + Invalidate(); +} + +ListBox::IndexType ListBox::GetItemsCount() const { + return static_cast( m_items.size() ); +} + +const sf::String& ListBox::GetItemText( IndexType index ) const { + if( index >= static_cast( m_items.size() ) || index < 0 ) { + return EMPTY; + } + + return m_items[ static_cast( index )]; +} + +const sf::String& ListBox::GetDisplayedItemText( IndexType index ) const { + if( index >= static_cast( m_items.size() ) || index < 0 ) { + return EMPTY; + } + + if(m_item_text_policy == ItemTextPolicy::RESIZE_LISTBOX) { + return GetItemText( index ); + } else { + return m_displayed_items_texts[ static_cast( index ) ]; + } +} + +ListBox::IndexType ListBox::GetHighlightedItem() const { + return m_highlighted_item; +} + +void ListBox::SetSelection( IndexType index ) { + if( m_selection_mode == SelectionMode::NO_SELECTION ) { + return; + } + + m_selected_items.clear(); + if( index != NONE ) { + m_selected_items.insert( index ); + } + + Invalidate(); +} + +void ListBox::SetSelection( std::initializer_list indices ) { + if( m_selection_mode == SelectionMode::MULTI_SELECTION ) { + m_selected_items = std::set( indices.begin(), indices.end() ); + + Invalidate(); + } else if( m_selection_mode == SelectionMode::SINGLE_SELECTION ) { + if( indices.size() > 0) { + SetSelection( *( indices.begin() ) ); + } else { + SetSelection( NONE ); + } + + Invalidate(); + } +} + +void ListBox::AppendToSelection( IndexType index ) { + if( m_selection_mode == SelectionMode::NO_SELECTION ) { + return; + } + + if( m_selected_items.size() == 0 || m_selection_mode == SelectionMode::MULTI_SELECTION ) { + m_selected_items.insert( index ); + } + + Invalidate(); +} + +void ListBox::RemoveFromSelection( IndexType index ) { + m_selected_items.erase( index ); + + Invalidate(); +} + +void ListBox::ClearSelection() { + m_selected_items.clear(); + + Invalidate(); +} + +bool ListBox::IsItemSelected(IndexType index) const { + return m_selected_items.find( index ) != m_selected_items.end(); +} + +ListBox::IndexType ListBox::GetSelectedItemsCount() const { + return static_cast( m_selected_items.size() ); +} + +ListBox::IndexType ListBox::GetSelectedItemIndex( IndexType index ) const { + if( index >= static_cast( m_selected_items.size() ) || index < 0 ) { + return NONE; + } + + auto it = m_selected_items.cbegin(); + std::advance( it, index ); + return *it; +} + +const sf::String& ListBox::GetSelectedItemText( IndexType index ) const { + return m_items[ static_cast( GetSelectedItemIndex( index ) ) ]; +} + +ListBox::IndexType ListBox::GetFirstDisplayedItemIndex() const { + return m_first_displayed_item; +} + +ListBox::IndexType ListBox::GetDisplayedItemsCount() const { + return std::min(m_max_displayed_items_count, static_cast( m_items.size() ) - m_first_displayed_item); +} + +ListBox::IndexType ListBox::GetMaxDisplayedItemsCount() const { + return m_max_displayed_items_count; +} + +ListBox::SelectionMode ListBox::GetSelectionMode() const { + return m_selection_mode; +} + +void ListBox::SetSelectionMode( SelectionMode mode ) { + m_selection_mode = mode; + + if( m_selection_mode == SelectionMode::NO_SELECTION ) { + m_selected_items.clear(); + Invalidate(); + } else if( m_selection_mode == SelectionMode::SINGLE_SELECTION && m_selected_items.size() > 1 ) { + auto it = m_selected_items.begin(); + std::advance( it, 1 ); + m_selected_items.erase( it, m_selected_items.end() ); + } +} + +ListBox::ScrollbarPolicy ListBox::GetScrollbarPolicy() const { + return m_scrollbar_policy; +} + +void ListBox::SetScrollbarPolicy( ScrollbarPolicy policy ) { + m_scrollbar_policy = policy; + + RequestResize(); + Invalidate(); +} + +ListBox::ItemTextPolicy ListBox::GetItemTextPolicy() const { + return m_item_text_policy; +} + +void ListBox::SetItemTextPolicy( ItemTextPolicy policy ) { + m_item_text_policy = policy; + + RequestResize(); + Invalidate(); +} + +void ListBox::HandleMouseEnter( int /*x*/, int /*y*/ ) { + if( !HasFocus() ) { + SetState( State::PRELIGHT ); + } +} + +void ListBox::HandleMouseLeave( int /*x*/, int /*y*/ ) { + if( !HasFocus() ) { + SetState( State::NORMAL ); + } +} + +void ListBox::HandleMouseMoveEvent( int x, int y ) { + if( ( x == std::numeric_limits::min() ) || ( y == std::numeric_limits::min() ) ) { + return; + } + + float realX = static_cast( x ) - GetAllocation().left; + float realY = static_cast( y ) - GetAllocation().top; + + if( IsMouseInWidget() && ( !IsScrollbarVisible() || ( IsScrollbarVisible() && realX < m_vertical_scrollbar->GetAllocation().left ) ) ) { + //Highlight the item under the mouse + IndexType hovered_item_index = GetItemAt( realY ); + m_highlighted_item = hovered_item_index; + + Invalidate(); + } else if( m_highlighted_item != NONE ) { + m_highlighted_item = NONE; + + Invalidate(); + } +} + +void ListBox::HandleMouseButtonEvent( sf::Mouse::Button button, bool press, int x, int y ) { + if( ( x == std::numeric_limits::min() ) || ( y == std::numeric_limits::min() ) ) { + return; + } + + float realX = static_cast( x ) - GetAllocation().left; + float realY = static_cast( y ) - GetAllocation().top; + + if( IsMouseInWidget() ) { + if( button == sf::Mouse::Left && press ) { + if( !IsScrollbarVisible() || ( IsScrollbarVisible() && realX < m_vertical_scrollbar->GetAllocation().left ) ) { + if( m_selection_mode != SelectionMode::NO_SELECTION ) { + // Find out and select which item has been clicked. + IndexType clicked_item_index = GetItemAt( realY ); + + if( clicked_item_index != NONE ) { // Only change the selection if the user has clicked on an item. + // Determine whether the clicked item is new to the selection. + bool selection_changed = false; + if( m_selection_mode == SelectionMode::SINGLE_SELECTION || ( !sf::Keyboard::isKeyPressed( sf::Keyboard::LControl ) && !sf::Keyboard::isKeyPressed( sf::Keyboard::RControl ) ) ) { + // In SINGLE_SELECTION mode or when Ctrl is not pressed, if the clicked item was not in the selected items list, the selection has changed. + selection_changed = std::find( m_selected_items.begin(), m_selected_items.end(), clicked_item_index ) == m_selected_items.end(); + + // Clear the selection and add the item to the selection. + m_selected_items.clear(); + m_selected_items.insert( clicked_item_index ); + } else { + // In MULTI_SELECTION and when Ctrl is pressed, the selection has changed (as if the clicked item is already selected, it is removed from the selection). + selection_changed = true; + + // Add or remove the clicked item, depending on if it was in the selection or not. + if(m_selected_items.find( clicked_item_index ) == m_selected_items.end() ) + m_selected_items.insert( clicked_item_index ); + else + m_selected_items.erase( clicked_item_index ); + } + + if( selection_changed ) // Only emit the OnSelect signal if the selection changed. + GetSignals().Emit( OnSelect ); + } + } + + GrabFocus(); + Invalidate(); + } + } + } +} + +ListBox::IndexType ListBox::GetItemAt( float y ) const { + auto text_padding = Context::Get().GetEngine().GetProperty( "Padding", shared_from_this() ); + auto border_width = Context::Get().GetEngine().GetProperty( "BorderWidth", shared_from_this() ); + const auto& font_name = Context::Get().GetEngine().GetProperty( "FontName", shared_from_this() ); + const auto& font = Context::Get().GetEngine().GetResourceManager().GetFont( font_name ); + auto font_size = Context::Get().GetEngine().GetProperty( "FontSize", shared_from_this() ); + auto line_height = Context::Get().GetEngine().GetFontLineHeight( *font, font_size ); + + IndexType item_index = 0; + while(y > border_width + static_cast( item_index ) * ( line_height + text_padding * 2 ) ) { + ++item_index; + } + + if( item_index == 0 ) + return 0 + m_first_displayed_item; + else if( item_index > GetDisplayedItemsCount() ) + return NONE; + else + return item_index - 1 + m_first_displayed_item; +} + +bool ListBox::IsScrollbarVisible() const { + return ( m_scrollbar_policy == ScrollbarPolicy::VERTICAL_ALWAYS || ( m_scrollbar_policy == ScrollbarPolicy::VERTICAL_AUTOMATIC && GetMaxDisplayedItemsCount() < GetItemsCount() ) ); +} + +void ListBox::HandleSizeChange() { + UpdateDisplayedItems(); + UpdateScrollbarAdjustment(); + UpdateScrollbarAllocation(); + + UpdateDisplayedItemsText(); + + Invalidate(); +} + +bool ListBox::HandleAdd( Widget::Ptr widget ) { + // The user can't add widgets to the ListBox. + + if( widget == m_vertical_scrollbar && GetChildren().size() == 0 ) { // The scrollbar is an exception (added while creating the widget). + Container::HandleAdd( widget ); + return true; + } + + #if defined( SFGUI_DEBUG ) + std::cerr << "SFGUI warning: No widgets can be added to a ListBox.\n"; + #endif + + return false; +} + +void ListBox::HandleRemove( Widget::Ptr /*widget*/ ){ + std::cerr << "SFGUI warning: No widgets can be removed from a ListBox.\n"; +} + +void ListBox::UpdateDisplayedItems() { + auto text_padding = Context::Get().GetEngine().GetProperty( "Padding", shared_from_this() ); + auto border_width = Context::Get().GetEngine().GetProperty( "BorderWidth", shared_from_this() ); + const auto& font_name = Context::Get().GetEngine().GetProperty( "FontName", shared_from_this() ); + const auto& font = Context::Get().GetEngine().GetResourceManager().GetFont( font_name ); + auto font_size = Context::Get().GetEngine().GetProperty( "FontSize", shared_from_this() ); + auto line_height = Context::Get().GetEngine().GetFontLineHeight( *font, font_size ); + + // Update the displayed items count. + float items_height = 0.f; + m_max_displayed_items_count = 0; + while(items_height < GetAllocation().height - border_width * 2.f + text_padding ) { + items_height += line_height + text_padding * 2.f; + ++m_max_displayed_items_count; + } + if(m_max_displayed_items_count > 0) + --m_max_displayed_items_count; + + // If there aren't enough items from m_first_displayed_item to + // m_first_displayed_item + m_max_displayed_items_count, decrement + // m_first_displayed_item. + if(m_first_displayed_item + m_max_displayed_items_count > static_cast( m_items.size() ) ) { + m_first_displayed_item = std::max( static_cast( m_items.size() ) - m_max_displayed_items_count, static_cast( 0 ) ); + } +} + +void ListBox::UpdateScrollbarAdjustment() { + m_vertical_scrollbar->GetAdjustment()->Configure( + static_cast( m_first_displayed_item ), + 0.f, + static_cast( m_items.size() ), + 1.f, + static_cast( m_max_displayed_items_count ), + static_cast( m_max_displayed_items_count ) + ); +} + +void ListBox::UpdateScrollbarAllocation() { + auto border_width = Context::Get().GetEngine().GetProperty( "BorderWidth", shared_from_this() ); + m_vertical_scrollbar->SetAllocation( sf::FloatRect( + GetAllocation().width - border_width - m_vertical_scrollbar->GetRequisition().x, + border_width, + m_vertical_scrollbar->GetRequisition().x, + GetAllocation().height - border_width * 2.f + ) ); + + m_vertical_scrollbar->RequestResize(); + m_vertical_scrollbar->Invalidate(); + m_vertical_scrollbar->Show( IsScrollbarVisible() ); + + Invalidate(); +} + +void ListBox::UpdateDisplayedItemsText() { + m_displayed_items_texts.clear(); + if(m_item_text_policy == ItemTextPolicy::RESIZE_LISTBOX) + return; + + auto text_padding = Context::Get().GetEngine().GetProperty( "Padding", shared_from_this() ); + auto border_width = Context::Get().GetEngine().GetProperty( "BorderWidth", shared_from_this() ); + const auto& font_name = Context::Get().GetEngine().GetProperty( "FontName", shared_from_this() ); + const auto& font = Context::Get().GetEngine().GetResourceManager().GetFont( font_name ); + auto font_size = Context::Get().GetEngine().GetProperty( "FontSize", shared_from_this() ); + auto dots_width = Context::Get().GetEngine().GetTextStringMetrics("...", *font, font_size).x; + + float max_width = GetAllocation().width - border_width * 2 - text_padding * 2 - ( IsScrollbarVisible() ? m_vertical_scrollbar->GetAllocation().width : 0 ); + + for(auto& itemText : m_items) { + if( Context::Get().GetEngine().GetTextStringMetrics( itemText, *font, font_size ).x < max_width ) { + // The item's text is fully displayable in the listbox's width. + m_displayed_items_texts.push_back( itemText ); + } else { + // We need to shrink the text so that it will fit inside the listbox (and don't forget some width for "..."). + sf::String displayableText; + + while( displayableText.getSize() < itemText.getSize() && + Context::Get().GetEngine().GetTextStringMetrics( displayableText, *font, font_size ).x <= max_width - dots_width ) { + displayableText += itemText[ displayableText.getSize() ]; + } + + if(displayableText.getSize() > 0) + displayableText.erase( displayableText.getSize() - 1 ); // Removes the last character as it is going outside of the available space. + + displayableText += "..."; + m_displayed_items_texts.push_back( displayableText ); + } + } + + Invalidate(); +} + +void ListBox::OnScrollbarChanged() { + m_first_displayed_item = static_cast( m_vertical_scrollbar->GetAdjustment()->GetValue() ); + UpdateDisplayedItems(); + + Invalidate(); +} + +}