mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-05-28 17:16:29 +03:00
Minor changes in DropDownList style.
Added StringMapModel and an example.
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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,<svg viewBox='0 0 24 24' fill='white'><path d='M12 15.6315L20.9679 10.8838L20.0321 9.11619L12 13.3685L3.96788 9.11619L3.0321 10.8838L12 15.6315Z'></path></svg>");
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
#include <eepp/ui/models/model.hpp>
|
||||
#include <eepp/ui/models/modelselection.hpp>
|
||||
#include <eepp/ui/models/sortingproxymodel.hpp>
|
||||
#include <eepp/ui/models/stringmapmodel.hpp>
|
||||
#include <eepp/ui/models/widgettreemodel.hpp>
|
||||
|
||||
#include <eepp/ui/uidatabind.hpp>
|
||||
|
||||
144
include/eepp/ui/models/stringmapmodel.hpp
Normal file
144
include/eepp/ui/models/stringmapmodel.hpp
Normal file
@@ -0,0 +1,144 @@
|
||||
#ifndef EE_UI_MODELS_STRINGMAPMODEL_HPP
|
||||
#define EE_UI_MODELS_STRINGMAPMODEL_HPP
|
||||
|
||||
#include <algorithm>
|
||||
#include <eepp/core/string.hpp>
|
||||
#include <eepp/ui/models/model.hpp>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
|
||||
namespace EE { namespace UI { namespace Models {
|
||||
|
||||
template <typename StringType = std::string> class StringMapModel : public Model {
|
||||
public:
|
||||
struct Node {
|
||||
const StringType* text;
|
||||
Node* parent = nullptr;
|
||||
std::vector<Node*> children;
|
||||
std::vector<Node*> visibleChildren;
|
||||
|
||||
Node( const StringType* text, Node* parent ) : text( text ), parent( parent ) {}
|
||||
|
||||
size_t childCount() const { return visibleChildren.size(); }
|
||||
};
|
||||
|
||||
static std::shared_ptr<StringMapModel<StringType>>
|
||||
create( const std::map<StringType, std::vector<StringType>>& map ) {
|
||||
return std::make_shared<StringMapModel<StringType>>( map );
|
||||
}
|
||||
|
||||
explicit StringMapModel( const std::map<StringType, std::vector<StringType>>& 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<Node*>( 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<Node*>( 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<Node*>( 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<Node*>( 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<StringType, std::vector<StringType>> mMap;
|
||||
StringType mEmptyString;
|
||||
std::deque<Node> mNodes;
|
||||
Node* mRoot{ nullptr };
|
||||
};
|
||||
|
||||
}}} // namespace EE::UI::Models
|
||||
|
||||
#endif // EE_UI_MODELS_STRINGMAPMODEL_HPP
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
39
src/examples/ui_treeview_model/treeviewmodel.cpp
Normal file
39
src/examples/ui_treeview_model/treeviewmodel.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#include <eepp/ee.hpp>
|
||||
|
||||
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<std::string, std::vector<std::string>> 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<std::string>::create( data );
|
||||
|
||||
UIWidget* vBox = app.getUI()->loadLayoutFromString( R"xml(
|
||||
<vbox layout_width="match_parent" layout_height="match_parent">
|
||||
<TextInput id="filter_input" layout_width="match_parent" layout_height="wrap_content" hint="Filter: " />
|
||||
<TreeView id="tree_view" layout_width="match_parent" layout_height="0dp" layout_weight="1" />
|
||||
</vbox>
|
||||
)xml" );
|
||||
|
||||
auto treeView = vBox->find<UITreeView>( "tree_view" );
|
||||
auto filterInput = vBox->find<UITextInput>( "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();
|
||||
}
|
||||
54
src/tests/unit_tests/stringmapmodel.cpp
Normal file
54
src/tests/unit_tests/stringmapmodel.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#include <eepp/ui/models/stringmapmodel.hpp>
|
||||
#include <eepp/ui/models/modelindex.hpp>
|
||||
#include "utest.hpp"
|
||||
|
||||
using namespace EE::UI::Models;
|
||||
|
||||
UTEST( StringMapModel, InitialState ) {
|
||||
std::map<std::string, std::vector<std::string>> data = {
|
||||
{ "A", { "1", "2" } },
|
||||
{ "B", { "3" } }
|
||||
};
|
||||
auto model = StringMapModel<std::string>::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<std::string, std::vector<std::string>> data = {
|
||||
{ "Cat", { "Item 1" } },
|
||||
{ "Dog", { "Item 2" } }
|
||||
};
|
||||
auto model = StringMapModel<std::string>::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 );
|
||||
}
|
||||
@@ -72,9 +72,12 @@ LLMChat::Role LLMChat::stringToRole( UIPushButton* userBut ) {
|
||||
static const char* DEFAULT_LAYOUT = R"xml(
|
||||
<style>
|
||||
.llm_chatui DropDownList {
|
||||
border: 0;
|
||||
border-color: transparent;
|
||||
background-color: var(--tab-back);
|
||||
}
|
||||
.llm_chatui DropDownList:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.llm_conversation {
|
||||
margin-bottom: 8dp;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user