diff --git a/.ecode/project_build.json b/.ecode/project_build.json index f05254b4e..60f6498ea 100644 --- a/.ecode/project_build.json +++ b/.ecode/project_build.json @@ -339,6 +339,12 @@ "command": "${project_root}/bin/eepp-richtext-debug", "name": "eepp-richtext-debug", "working_dir": "${project_root}/bin" + }, + { + "args": "", + "command": "${project_root}/bin/eepp-treeviewmodel-debug", + "name": "eepp-treeviewmodel-debug", + "working_dir": "${project_root}/bin" } ], "var": { diff --git a/bin/assets/ui/breeze.css b/bin/assets/ui/breeze.css index 014e7bcd7..9fb5c6db3 100644 --- a/bin/assets/ui/breeze.css +++ b/bin/assets/ui/breeze.css @@ -687,6 +687,11 @@ Window::border::bottom { background-color: var(--separator); } +DropDownList { + padding-right: 16dp; + text-overflow: ellipsis; +} + DropDownList, ComboBox::Button { foreground-image: url("data:image/svg,"); diff --git a/include/eepp/ui.hpp b/include/eepp/ui.hpp index a35f8d276..ece198740 100644 --- a/include/eepp/ui.hpp +++ b/include/eepp/ui.hpp @@ -86,6 +86,7 @@ #include #include #include +#include #include #include diff --git a/include/eepp/ui/models/stringmapmodel.hpp b/include/eepp/ui/models/stringmapmodel.hpp new file mode 100644 index 000000000..f653f95e9 --- /dev/null +++ b/include/eepp/ui/models/stringmapmodel.hpp @@ -0,0 +1,144 @@ +#ifndef EE_UI_MODELS_STRINGMAPMODEL_HPP +#define EE_UI_MODELS_STRINGMAPMODEL_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace EE { namespace UI { namespace Models { + +template class StringMapModel : public Model { + public: + struct Node { + const StringType* text; + Node* parent = nullptr; + std::vector children; + std::vector visibleChildren; + + Node( const StringType* text, Node* parent ) : text( text ), parent( parent ) {} + + size_t childCount() const { return visibleChildren.size(); } + }; + + static std::shared_ptr> + create( const std::map>& map ) { + return std::make_shared>( map ); + } + + explicit StringMapModel( const std::map>& map ) : mMap( map ) { + mNodes.emplace_back( &mEmptyString, nullptr ); + mRoot = &mNodes.back(); + + for ( const auto& [category, items] : mMap ) { + mNodes.emplace_back( &category, mRoot ); + Node* catNode = &mNodes.back(); + mRoot->children.push_back( catNode ); + + for ( const auto& item : items ) { + mNodes.emplace_back( &item, catNode ); + catNode->children.push_back( &mNodes.back() ); + } + } + resetFilter(); + } + + virtual ~StringMapModel() {} + + virtual size_t rowCount( const ModelIndex& parent = ModelIndex() ) const override { + if ( !parent.isValid() ) + return mRoot->visibleChildren.size(); + Node* node = static_cast( parent.internalData() ); + return node->visibleChildren.size(); + } + + virtual size_t columnCount( const ModelIndex& = ModelIndex() ) const override { return 1; } + + virtual ModelIndex index( int row, int column, + const ModelIndex& parent = ModelIndex() ) const override { + if ( row < 0 || column < 0 || row >= (int)rowCount( parent ) || + column >= (int)columnCount( parent ) ) + return {}; + Node* parentNode = + parent.isValid() ? static_cast( parent.internalData() ) : mRoot; + if ( row < (int)parentNode->visibleChildren.size() ) + return createIndex( row, column, parentNode->visibleChildren[row] ); + return {}; + } + + virtual ModelIndex parent( const ModelIndex& index ) const { + if ( !index.isValid() ) + return {}; + Node* node = static_cast( index.internalData() ); + if ( node->parent == mRoot || node->parent == nullptr ) + return {}; + Node* grandParent = node->parent->parent; + if ( !grandParent ) + return {}; + // Find row of parent in grandparent's visible children + auto it = std::find( grandParent->visibleChildren.begin(), + grandParent->visibleChildren.end(), node->parent ); + if ( it != grandParent->visibleChildren.end() ) { + int row = std::distance( grandParent->visibleChildren.begin(), it ); + return createIndex( row, 0, node->parent ); + } + return {}; + } + + virtual Variant data( const ModelIndex& index, ModelRole role = ModelRole::Display ) const override { + if ( !index.isValid() ) + return {}; + if ( role == ModelRole::Display ) { + Node* node = static_cast( index.internalData() ); + return Variant( *node->text ); + } + return {}; + } + + void filter( const std::string_view& filterText ) { + if ( filterText.empty() ) { + resetFilter(); + return; + } + mRoot->visibleChildren.clear(); + for ( const auto& cat : mRoot->children ) { + bool catMatches = String::icontains( *cat->text, filterText ); + cat->visibleChildren.clear(); + for ( const auto& item : cat->children ) { + if ( catMatches || String::icontains( *item->text, filterText ) ) { + cat->visibleChildren.push_back( item ); + } + } + + if ( catMatches || !cat->visibleChildren.empty() ) { + mRoot->visibleChildren.push_back( cat ); + } + } + invalidate( Model::UpdateFlag::InvalidateAllIndexes ); + } + + void resetFilter() { + mRoot->visibleChildren.clear(); + for ( const auto& cat : mRoot->children ) { + cat->visibleChildren.clear(); + for ( const auto& item : cat->children ) { + cat->visibleChildren.push_back( item ); + } + mRoot->visibleChildren.push_back( cat ); + } + invalidate( Model::UpdateFlag::InvalidateAllIndexes ); + } + + private: + std::map> mMap; + StringType mEmptyString; + std::deque mNodes; + Node* mRoot{ nullptr }; +}; + +}}} // namespace EE::UI::Models + +#endif // EE_UI_MODELS_STRINGMAPMODEL_HPP diff --git a/premake4.lua b/premake4.lua index 760cb6929..a667289de 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1607,6 +1607,12 @@ solution "eepp" files { "src/examples/7guis/cells/*.cpp" } build_link_configuration( "eepp-7guis-cells", true ) + project "eepp-treeviewmodel" + set_kind() + language "C++" + files { "src/examples/ui_treeview_model/*.cpp" } + build_link_configuration( "eepp-treeviewmodel", true ) + -- Tools project "eepp-textureatlaseditor" set_kind() diff --git a/premake5.lua b/premake5.lua index 56c708686..c7aca6939 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1483,6 +1483,12 @@ workspace "eepp" files { "src/examples/7guis/cells/*.cpp" } build_link_configuration( "eepp-7guis-cells", true ) + project "eepp-treeviewmodel" + set_kind() + language "C++" + files { "src/examples/ui_treeview_model/*.cpp" } + build_link_configuration( "eepp-treeviewmodel", true ) + -- Tools project "eepp-textureatlaseditor" set_kind() diff --git a/src/examples/ui_treeview_model/treeviewmodel.cpp b/src/examples/ui_treeview_model/treeviewmodel.cpp new file mode 100644 index 000000000..6d01bf531 --- /dev/null +++ b/src/examples/ui_treeview_model/treeviewmodel.cpp @@ -0,0 +1,39 @@ +#include + +using namespace EE; +using namespace EE::UI; +using namespace EE::UI::Models; + +EE_MAIN_FUNC int main( int, char** ) { + UIApplication app( { 800, 600, "eepp - StringMapModel Example" } ); + + std::map> data = { + { "Category 1", { "Item 1.1", "Item 1.2", "Item 1.3" } }, + { "Category 2", { "Item 2.1", "Item 2.2", "Something else" } }, + { "Fruits", { "Apple", "Banana", "Orange", "Grape" } }, + { "Programming Languages", { "C++", "Lua", "Python", "Rust" } } }; + + auto model = StringMapModel::create( data ); + + UIWidget* vBox = app.getUI()->loadLayoutFromString( R"xml( + + + + + )xml" ); + + auto treeView = vBox->find( "tree_view" ); + auto filterInput = vBox->find( "filter_input" ); + + treeView->setHeadersVisible( false ); + treeView->setAutoExpandOnSingleColumn( true ); + treeView->setModel( model ); + treeView->expandAll(); + + filterInput->on( Event::OnTextChanged, [model, filterInput]( const Event* ) { + model->filter( filterInput->getText().toUtf8() ); + } ); + filterInput->setFocus(); + + return app.run(); +} diff --git a/src/tests/unit_tests/stringmapmodel.cpp b/src/tests/unit_tests/stringmapmodel.cpp new file mode 100644 index 000000000..a9d932403 --- /dev/null +++ b/src/tests/unit_tests/stringmapmodel.cpp @@ -0,0 +1,54 @@ +#include +#include +#include "utest.hpp" + +using namespace EE::UI::Models; + +UTEST( StringMapModel, InitialState ) { + std::map> data = { + { "A", { "1", "2" } }, + { "B", { "3" } } + }; + auto model = StringMapModel::create( data ); + + ASSERT_EQ( (int)model->rowCount(), 2 ); + ASSERT_EQ( (int)model->columnCount(), 1 ); + + ModelIndex idxA = model->index( 0, 0 ); + ModelIndex idxB = model->index( 1, 0 ); + + ASSERT_TRUE( idxA.isValid() ); + ASSERT_TRUE( idxB.isValid() ); + + // Ordering is map ordering, so A then B. + ASSERT_STDSTREQ( model->data( idxA ).asStdString(), "A" ); + ASSERT_STDSTREQ( model->data( idxB ).asStdString(), "B" ); + + ASSERT_EQ( (int)model->rowCount( idxA ), 2 ); + ASSERT_EQ( (int)model->rowCount( idxB ), 1 ); + + ModelIndex idxA1 = model->index( 0, 0, idxA ); + ASSERT_TRUE( idxA1.isValid() ); + ASSERT_STDSTREQ( model->data( idxA1 ).asStdString(), "1" ); +} + +UTEST( StringMapModel, Filter ) { + std::map> data = { + { "Cat", { "Item 1" } }, + { "Dog", { "Item 2" } } + }; + auto model = StringMapModel::create( data ); + + model->filter( "Cat" ); + ASSERT_EQ( (int)model->rowCount(), 1 ); + ModelIndex idx = model->index( 0, 0 ); + ASSERT_STDSTREQ( model->data( idx ).asStdString(), "Cat" ); + + model->filter( "Item 2" ); + ASSERT_EQ( (int)model->rowCount(), 1 ); + idx = model->index( 0, 0 ); + ASSERT_STDSTREQ( model->data( idx ).asStdString(), "Dog" ); + + model->filter( "" ); + ASSERT_EQ( (int)model->rowCount(), 2 ); +} diff --git a/src/tools/ecode/plugins/aiassistant/chatui.cpp b/src/tools/ecode/plugins/aiassistant/chatui.cpp index 6fceaa779..e15f7db50 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.cpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.cpp @@ -72,9 +72,12 @@ LLMChat::Role LLMChat::stringToRole( UIPushButton* userBut ) { static const char* DEFAULT_LAYOUT = R"xml(