From 05d6d3e2a39de10b48dcb1c147e5335b05e52faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 21 Mar 2026 15:02:50 -0300 Subject: [PATCH] Redesigned LLM model selection in AI Assistant chat UI (now it's possible to search by filtering its name). Redesigned UIDropDownList to inherit from UIDropDown which is a base class to handle different types of drop-downs. Added UIDropDownModelList which is the same as UIDropDownList but uses a UIListView by default so it's a model/view based DropDown. Fix crash when changing states of the buttons in the build panel. Increased the default animations speed. --- .ecode/project_build.json | 6 + bin/assets/ui/breeze.css | 12 +- .../eepp/ui/abstract/uiabstracttableview.hpp | 2 + include/eepp/ui/uidropdown.hpp | 89 ++++ include/eepp/ui/uidropdownlist.hpp | 72 +--- include/eepp/ui/uidropdownmodellist.hpp | 68 ++++ include/eepp/ui/uihelper.hpp | 2 + include/eepp/ui/uilistview.hpp | 4 +- premake4.lua | 6 + premake5.lua | 6 + src/eepp/ui/doc/languages/css.cpp | 2 + src/eepp/ui/uidropdown.cpp | 339 ++++++++++++++++ src/eepp/ui/uidropdownlist.cpp | 331 ++------------- src/eepp/ui/uidropdownmodellist.cpp | 267 ++++++++++++ src/eepp/ui/uilistview.cpp | 6 +- src/eepp/ui/uithememanager.cpp | 4 +- src/eepp/ui/uiwidgetcreator.cpp | 2 + .../ui_dropdownmodellist.cpp | 51 +++ src/tests/unit_tests/uidropdownmodellist.cpp | 43 ++ .../plugins/aiassistant/aiassistantplugin.cpp | 5 +- .../ecode/plugins/aiassistant/chatui.cpp | 383 +++++++++++++----- .../ecode/plugins/aiassistant/chatui.hpp | 30 +- .../ecode/plugins/aiassistant/protocol.hpp | 1 + .../ecode/statusbuildoutputcontroller.cpp | 16 +- 24 files changed, 1274 insertions(+), 473 deletions(-) create mode 100644 include/eepp/ui/uidropdown.hpp create mode 100644 include/eepp/ui/uidropdownmodellist.hpp create mode 100644 src/eepp/ui/uidropdown.cpp create mode 100644 src/eepp/ui/uidropdownmodellist.cpp create mode 100644 src/examples/ui_dropdownmodellist/ui_dropdownmodellist.cpp create mode 100644 src/tests/unit_tests/uidropdownmodellist.cpp diff --git a/.ecode/project_build.json b/.ecode/project_build.json index e1b483ec7..b32e7a0e1 100644 --- a/.ecode/project_build.json +++ b/.ecode/project_build.json @@ -369,6 +369,12 @@ "command": "${project_root}/bin/eepp-ui-markdownview-debug", "name": "eepp-ui-markdownview-debug", "working_dir": "${project_root}/bin" + }, + { + "args": "", + "command": "${project_root}/bin/eepp-ui-dropdownmodellist-debug", + "name": "eepp-ui-dropdownmodellist-debug", + "working_dir": "${project_root}/bin" } ], "var": { diff --git a/bin/assets/ui/breeze.css b/bin/assets/ui/breeze.css index f0758618a..49e06d3fc 100644 --- a/bin/assets/ui/breeze.css +++ b/bin/assets/ui/breeze.css @@ -244,7 +244,8 @@ Anchor:hover { PushButton, SelectButton, -DropDownList { +DropDownList, +DropDownModelList { padding-left: var(--base-horizontal-padding); padding-right: var(--base-horizontal-padding); padding-top: var(--base-vertical-padding); @@ -259,6 +260,7 @@ DropDownList { } DropDownList, +DropDownModelList, ComboBox::DropDownList { max-visible-items: 6; } @@ -267,6 +269,8 @@ PushButton:hover, PushButton:focus, DropDownList:hover, DropDownList:focus, +DropDownModelList:hover, +DropDownModelList:focus, SelectButton:hover, SelectButton:focus, ComboBox:hover, @@ -800,12 +804,14 @@ Window::border::bottom { background-color: var(--separator); } -DropDownList { +DropDownList, +DropDownModelList { padding-right: 16dp; text-overflow: ellipsis; } DropDownList, +DropDownModelList, ComboBox::Button { foreground-image: url("data:image/svg,"); foreground-position-x: right 6dp; @@ -816,6 +822,8 @@ ComboBox::Button { DropDownList:hover, DropDownList:focus, +DropDownModelList:hover, +DropDownModelList:focus, ComboBox::Button:focus, ComboBox::Button:hover { foreground-tint: var(--icon-active); diff --git a/include/eepp/ui/abstract/uiabstracttableview.hpp b/include/eepp/ui/abstract/uiabstracttableview.hpp index 9c85f953a..4df910bc2 100644 --- a/include/eepp/ui/abstract/uiabstracttableview.hpp +++ b/include/eepp/ui/abstract/uiabstracttableview.hpp @@ -13,6 +13,7 @@ using namespace EE::Math; namespace EE { namespace UI { class UIPushButton; class UILinearLayout; +class UIDropDownModelList; }} // namespace EE::UI namespace EE { namespace UI { namespace Abstract { @@ -146,6 +147,7 @@ class EE_API UIAbstractTableView : public UIAbstractView { protected: friend class EE::UI::UITableHeaderColumn; + friend class EE::UI::UIDropDownModelList; struct ColumnData { Float minWidth{ 0 }; diff --git a/include/eepp/ui/uidropdown.hpp b/include/eepp/ui/uidropdown.hpp new file mode 100644 index 000000000..06659d6db --- /dev/null +++ b/include/eepp/ui/uidropdown.hpp @@ -0,0 +1,89 @@ +#ifndef EE_UI_UIDROPDOWN_HPP +#define EE_UI_UIDROPDOWN_HPP + +#include + +namespace EE { namespace UI { + +class EE_API UIDropDown : public UITextInput { + public: + enum class MenuWidthMode { + DropDown, + Contents, + ContentsCentered, + ExpandIfNeeded, + ExpandIfNeededCentered + }; + + static MenuWidthMode menuWidthModeFromString( std::string_view str ); + + static std::string menuWidthModeToString( MenuWidthMode rule ); + + struct StyleConfig { + Uint32 MaxNumVisibleItems = 10; + bool PopUpToRoot = false; + MenuWidthMode menuWidthRule{ MenuWidthMode::DropDown }; + }; + + virtual ~UIDropDown(); + + virtual Uint32 getType() const; + virtual bool isType( const Uint32& type ) const; + + virtual void setTheme( UITheme* Theme ); + + virtual UIDropDown* showList(); + + bool getPopUpToRoot() const; + UIDropDown* setPopUpToRoot( bool popUpToRoot ); + + Uint32 getMaxNumVisibleItems() const; + virtual UIDropDown* setMaxNumVisibleItems( const Uint32& maxNumVisibleItems ); + + const StyleConfig& getStyleConfig() const; + UIDropDown* setStyleConfig( const StyleConfig& styleConfig ); + + UIDropDown* setMenuWidthMode( MenuWidthMode rule ); + MenuWidthMode getMenuWidthMode() const; + + virtual bool applyProperty( const StyleSheetProperty& attribute ); + virtual std::string getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex = 0 ) const; + virtual std::vector getPropertiesImplemented() const; + + protected: + StyleConfig mStyleConfig; + UINode* mFriendNode{ nullptr }; + + UIDropDown( const std::string& tag ); + + virtual UIWidget* getPopUpWidget() const; + + void onPopUpFocusLoss( const Event* Event ); + + virtual void onItemSelected( const Event* Event ); + virtual void show(); + virtual void hide(); + + virtual Uint32 onMouseOver( const Vector2i& position, const Uint32& flags ); + virtual Uint32 onMouseLeave( const Vector2i& position, const Uint32& flags ); + virtual Uint32 onMouseClick( const Vector2i& position, const Uint32& flags ); + + virtual void onItemClicked( const Event* Event ); + virtual void onItemKeyDown( const Event* Event ); + virtual void onWidgetClear( const Event* Event ); + virtual Uint32 onKeyDown( const KeyEvent& Event ); + + virtual void onSizeChange(); + virtual void onAutoSize(); + virtual void onThemeLoaded(); + + void setFriendNode( UINode* friendNode ); + + Float getPopUpWidth( Float contentsWidth ) const; + void alignPopUp( UIWidget* widget ); +}; + +}} // namespace EE::UI + +#endif diff --git a/include/eepp/ui/uidropdownlist.hpp b/include/eepp/ui/uidropdownlist.hpp index 66f1a619e..2ed5f4daa 100644 --- a/include/eepp/ui/uidropdownlist.hpp +++ b/include/eepp/ui/uidropdownlist.hpp @@ -1,30 +1,15 @@ #ifndef EE_UICUIDROPDOWNLIST_HPP #define EE_UICUIDROPDOWNLIST_HPP +#include #include -#include namespace EE { namespace UI { -class EE_API UIDropDownList : public UITextInput { +class EE_API UIDropDownList : public UIDropDown { public: - enum class MenuWidthMode { - DropDown, - Contents, - ContentsCentered, - ExpandIfNeeded, - ExpandIfNeededCentered - }; - - static MenuWidthMode menuWidthModeFromString( std::string_view str ); - - static std::string menuWidthModeToString( MenuWidthMode rule ); - - struct StyleConfig { - Uint32 MaxNumVisibleItems = 10; - bool PopUpToRoot = false; - MenuWidthMode menuWidthRule{ MenuWidthMode::DropDown }; - }; + using MenuWidthMode = UIDropDown::MenuWidthMode; + using StyleConfig = UIDropDown::StyleConfig; static UIDropDownList* NewWithTag( const std::string& tag ); @@ -33,84 +18,39 @@ class EE_API UIDropDownList : public UITextInput { virtual ~UIDropDownList(); virtual Uint32 getType() const; - virtual bool isType( const Uint32& type ) const; - virtual void setTheme( UITheme* Theme ); - UIListBox* getListBox() const; UIDropDownList* showList(); - bool getPopUpToRoot() const; - - UIDropDownList* setPopUpToRoot( bool popUpToRoot ); - - Uint32 getMaxNumVisibleItems() const; - - UIDropDownList* setMaxNumVisibleItems( const Uint32& maxNumVisibleItems ); - - const StyleConfig& getStyleConfig() const; - - UIDropDownList* setStyleConfig( const StyleConfig& styleConfig ); + virtual UIDropDownList* setMaxNumVisibleItems( const Uint32& maxNumVisibleItems ); virtual bool applyProperty( const StyleSheetProperty& attribute ); - virtual std::string getPropertyString( const PropertyDefinition* propertyDef, const Uint32& propertyIndex = 0 ) const; - virtual std::vector getPropertiesImplemented() const; virtual void loadFromXmlNode( const pugi::xml_node& node ); - UIDropDownList* setMenuWidthMode( MenuWidthMode rule ); - - MenuWidthMode getMenuWidthMode() const; - protected: friend class UIComboBox; - StyleConfig mStyleConfig; UIListBox* mListBox; - UINode* mFriendNode; Uint32 mListBoxCloseCb{ 0 }; UIDropDownList( const std::string& tag = "dropdownlist" ); - void onListBoxFocusLoss( const Event* Event ); + virtual UIWidget* getPopUpWidget() const; virtual void onItemSelected( const Event* Event ); - virtual void show(); - - virtual void hide(); - - virtual Uint32 onMouseOver( const Vector2i& position, const Uint32& flags ); - - virtual Uint32 onMouseLeave( const Vector2i& position, const Uint32& flags ); - virtual Uint32 onMouseUp( const Vector2i& position, const Uint32& flags ); - virtual Uint32 onMouseClick( const Vector2i& position, const Uint32& flags ); - - virtual void onItemClicked( const Event* Event ); - - virtual void onItemKeyDown( const Event* Event ); - - virtual void onWidgetClear( const Event* Event ); - virtual Uint32 onKeyDown( const KeyEvent& Event ); virtual void onClassChange(); - virtual void onSizeChange(); - - virtual void onAutoSize(); - - virtual void onThemeLoaded(); - - void setFriendNode( UINode* friendNode ); - void destroyListBox(); }; diff --git a/include/eepp/ui/uidropdownmodellist.hpp b/include/eepp/ui/uidropdownmodellist.hpp new file mode 100644 index 000000000..4fa61f217 --- /dev/null +++ b/include/eepp/ui/uidropdownmodellist.hpp @@ -0,0 +1,68 @@ +#ifndef EE_UI_UIDROPDOWNMODELLIST_HPP +#define EE_UI_UIDROPDOWNMODELLIST_HPP + +#include +#include + +namespace EE { namespace UI { + +class EE_API UIDropDownModelList : public UIDropDown { + public: + using MenuWidthMode = UIDropDown::MenuWidthMode; + using StyleConfig = UIDropDown::StyleConfig; + + static UIDropDownModelList* NewWithTag( const std::string& tag ); + + static UIDropDownModelList* New(); + + virtual ~UIDropDownModelList(); + + virtual Uint32 getType() const; + virtual bool isType( const Uint32& type ) const; + + UIAbstractTableView* getListView() const; + + void setListView( UIAbstractTableView* listView ); + + std::shared_ptr getModel() const; + + virtual void setModel( std::shared_ptr model ); + + UIDropDownModelList* showList(); + + virtual UIDropDownModelList* setMaxNumVisibleItems( const Uint32& maxNumVisibleItems ); + + virtual std::string getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex = 0 ) const; + + virtual std::vector getPropertiesImplemented() const; + + protected: + UIAbstractTableView* mListView; + Uint32 mListViewCloseCb{ 0 }; + std::shared_ptr mModel; + + UIDropDownModelList( const std::string& tag = "dropdownmodellist" ); + + virtual UIWidget* getPopUpWidget() const; + + virtual void onItemSelected( const Event* Event ); + + virtual Uint32 onMouseUp( const Vector2i& position, const Uint32& flags ); + + virtual Uint32 onKeyDown( const KeyEvent& Event ); + + virtual void onItemClicked( const Event* Event ); + + virtual void onClassChange(); + + void destroyListView(); + + UIWidget* createDefaultListView(); + + void updateSelectionIndex(); +}; + +}} // namespace EE::UI + +#endif diff --git a/include/eepp/ui/uihelper.hpp b/include/eepp/ui/uihelper.hpp index 7e87df911..e1aa2bc5b 100644 --- a/include/eepp/ui/uihelper.hpp +++ b/include/eepp/ui/uihelper.hpp @@ -65,6 +65,7 @@ enum UINodeType { UI_TYPE_PROGRESSBAR, UI_TYPE_LISTBOX, UI_TYPE_LISTBOXITEM, + UI_TYPE_DROPDOWN, UI_TYPE_DROPDOWNLIST, UI_TYPE_MENU_SEPARATOR, UI_TYPE_COMBOBOX, @@ -117,6 +118,7 @@ enum UINodeType { UI_TYPE_HTML_TABLE_FOOTER, UI_TYPE_HTML_TABLE_ROW, UI_TYPE_HTML_TABLE_CELL, + UI_TYPE_DROPDOWNMODELLIST, UI_TYPE_MODULES = 10000, UI_TYPE_TERMINAL = 10001, UI_TYPE_USER = 200000, diff --git a/include/eepp/ui/uilistview.hpp b/include/eepp/ui/uilistview.hpp index b80d89237..c1c933d30 100644 --- a/include/eepp/ui/uilistview.hpp +++ b/include/eepp/ui/uilistview.hpp @@ -9,6 +9,8 @@ class EE_API UIListView : public UITableView { public: static UIListView* New(); + static UIListView* NewWithTag( const std::string& tag ); + Uint32 getType() const; bool isType( const Uint32& type ) const; @@ -16,7 +18,7 @@ class EE_API UIListView : public UITableView { void setTheme( UITheme* Theme ); protected: - UIListView(); + UIListView( const std::string& tag = "listview" ); }; }} // namespace EE::UI diff --git a/premake4.lua b/premake4.lua index a864173f7..ef9f966d1 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1580,6 +1580,12 @@ solution "eepp" files { "src/examples/ui_application_hello_world/*.cpp" } build_link_configuration( "eepp-ui-application-hello-world", true ) + project "eepp-ui-dropdownmodellist" + set_kind() + language "C++" + files { "src/examples/ui_dropdownmodellist/*.cpp" } + build_link_configuration( "eepp-ui-dropdownmodellist", true ) + project "eepp-ui-richtext" set_kind() language "C++" diff --git a/premake5.lua b/premake5.lua index 124efc44e..38a5aef5b 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1466,6 +1466,12 @@ workspace "eepp" files { "src/examples/ui_application_hello_world/*.cpp" } build_link_configuration( "eepp-ui-application-hello-world", true ) + project "eepp-ui-dropdownmodellist" + set_kind() + language "C++" + files { "src/examples/ui_dropdownmodellist/*.cpp" } + build_link_configuration( "eepp-ui-dropdownmodellist", true ) + project "eepp-ui-richtext" set_kind() language "C++" diff --git a/src/eepp/ui/doc/languages/css.cpp b/src/eepp/ui/doc/languages/css.cpp index adee86d4f..c98e26a4a 100644 --- a/src/eepp/ui/doc/languages/css.cpp +++ b/src/eepp/ui/doc/languages/css.cpp @@ -384,6 +384,7 @@ void addCSS() { { "RadioButton", "keyword" }, { "ComboBox", "keyword" }, { "DropDownList", "keyword" }, + { "DropDownModelList", "keyword" }, { "Image", "keyword" }, { "ListBox", "keyword" }, { "MenuBar", "keyword" }, @@ -425,6 +426,7 @@ void addCSS() { { "PopUpMenu", "keyword" }, { "ImageViewer", "keyword" }, { "AudioPlayer", "keyword" }, + { "Table", "keyword" }, }, "", diff --git a/src/eepp/ui/uidropdown.cpp b/src/eepp/ui/uidropdown.cpp new file mode 100644 index 000000000..7186e574e --- /dev/null +++ b/src/eepp/ui/uidropdown.cpp @@ -0,0 +1,339 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace EE { namespace UI { + +UIDropDown::MenuWidthMode UIDropDown::menuWidthModeFromString( std::string_view str ) { + if ( "contents" == str || "fit-to-contents" == str ) + return MenuWidthMode::Contents; + if ( "contents-centered" == str || "fit-to-contents-centered" == str ) + return MenuWidthMode::ContentsCentered; + if ( "expand-if-needed" == str || "fit-to-drop-down-expand-if-needed" == str ) + return MenuWidthMode::ExpandIfNeeded; + if ( "expand-if-needed-centered" == str || "fit-to-drop-down-expand-if-needed-centered" == str ) + return MenuWidthMode::ExpandIfNeededCentered; + return MenuWidthMode::DropDown; // "dropdown" +} + +std::string UIDropDown::menuWidthModeToString( MenuWidthMode rule ) { + switch ( rule ) { + case MenuWidthMode::DropDown: + return "dropdown"; + case MenuWidthMode::Contents: + return "contents"; + case MenuWidthMode::ContentsCentered: + return "contents-centered"; + case MenuWidthMode::ExpandIfNeeded: + return "expand-if-needed"; + case MenuWidthMode::ExpandIfNeededCentered: + return "expand-if-needed-centered"; + } + return "dropdown"; +} + +UIDropDown::UIDropDown( const std::string& tag ) : UITextInput( tag ) { + mEnabledCreateContextMenu = false; + setClipType( ClipType::ContentBox ); + setFlags( UI_AUTO_SIZE | UI_AUTO_PADDING | UI_SCROLLABLE ); + unsetFlags( UI_TEXT_SELECTION_ENABLED ); + setAllowEditing( false ); +} + +UIDropDown::~UIDropDown() {} + +Uint32 UIDropDown::getType() const { + return UI_TYPE_DROPDOWN; +} + +bool UIDropDown::isType( const Uint32& type ) const { + return UIDropDown::getType() == type ? true : UITextInput::isType( type ); +} + +void UIDropDown::setTheme( UITheme* Theme ) { + UIWidget::setTheme( Theme ); + setThemeSkin( Theme, "dropdownlist" ); + onThemeLoaded(); +} + +void UIDropDown::onSizeChange() { + onAutoSize(); + UITextInput::onSizeChange(); +} + +void UIDropDown::onThemeLoaded() { + autoPadding(); + onAutoSize(); +} + +void UIDropDown::setFriendNode( UINode* friendNode ) { + mFriendNode = friendNode; +} + +void UIDropDown::onAutoSize() { + Float max = eemax( PixelDensity::dpToPxI( getSkinSize().getHeight() ), + mTextCache.getLineSpacing() ); + + if ( mHeightPolicy == SizePolicy::WrapContent ) { + setInternalPixelsHeight( eeceil( max + mPaddingPx.Top + mPaddingPx.Bottom ) ); + } else if ( ( ( mFlags & UI_AUTO_SIZE ) || 0 == getSize().getHeight() ) && max > 0 ) { + setInternalPixelsHeight( eeceil( max ) ); + } +} + +UIWidget* UIDropDown::getPopUpWidget() const { + return nullptr; +} + +Uint32 UIDropDown::onMouseClick( const Vector2i& Pos, const Uint32& Flags ) { + if ( ( Flags & EE_BUTTON_LMASK ) && NULL == mFriendNode ) + showList(); + + if ( NULL != mFriendNode ) { + UITextInput::onMouseClick( Pos, Flags ); + } + + return 1; +} + +UIDropDown* UIDropDown::showList() { + return this; +} + +Float UIDropDown::getPopUpWidth( Float contentsWidth ) const { + Float width = NULL != mFriendNode ? mFriendNode->getSize().getWidth() : getSize().getWidth(); + + if ( mStyleConfig.menuWidthRule == MenuWidthMode::Contents || + mStyleConfig.menuWidthRule == MenuWidthMode::ContentsCentered ) { + width = contentsWidth; + } + + if ( ( mStyleConfig.menuWidthRule == MenuWidthMode::ExpandIfNeeded || + mStyleConfig.menuWidthRule == MenuWidthMode::ExpandIfNeededCentered ) && + contentsWidth > width ) { + width = contentsWidth; + } + + return width; +} + +void UIDropDown::alignPopUp( UIWidget* widget ) { + if ( !mStyleConfig.PopUpToRoot ) + widget->setParent( getWindowContainer() ); + else + widget->setParent( getUISceneNode()->getRoot() ); + + widget->toFront(); + + bool center = mStyleConfig.menuWidthRule == MenuWidthMode::ContentsCentered || + mStyleConfig.menuWidthRule == MenuWidthMode::ExpandIfNeededCentered; + + Float width = widget->getSize().getWidth(); + Float offsetX = center ? eefloor( ( getSize().getWidth() - width ) * 0.5f ) : 0; + + Vector2f pos( mDpPos.x + offsetX, mDpPos.y + getSize().getHeight() ); + Vector2f posCpy( pos ); + nodeToWorld( posCpy ); + + if ( !getUISceneNode()->getWorldBounds().contains( Rectf( posCpy, widget->getSize() ) ) ) { + pos = Vector2f( mDpPos.x + offsetX, mDpPos.y - widget->getSize().getHeight() ); + } + + if ( mStyleConfig.PopUpToRoot ) { + getParent()->nodeToWorld( pos ); + pos = PixelDensity::pxToDp( pos ); + } else { + Node* parentNode = getParent(); + Node* rp = getWindowContainer(); + while ( rp != parentNode ) { + pos += parentNode->getPosition(); + parentNode = parentNode->getParent(); + } + } + + widget->setPosition( pos ); + show(); + widget->setFocus(); +} + +bool UIDropDown::getPopUpToRoot() const { + return mStyleConfig.PopUpToRoot; +} + +UIDropDown* UIDropDown::setPopUpToRoot( bool popUpToRoot ) { + mStyleConfig.PopUpToRoot = popUpToRoot; + return this; +} + +Uint32 UIDropDown::getMaxNumVisibleItems() const { + return mStyleConfig.MaxNumVisibleItems; +} + +UIDropDown* UIDropDown::setMaxNumVisibleItems( const Uint32& maxNumVisibleItems ) { + mStyleConfig.MaxNumVisibleItems = maxNumVisibleItems; + return this; +} + +const UIDropDown::StyleConfig& UIDropDown::getStyleConfig() const { + return mStyleConfig; +} + +UIDropDown* UIDropDown::setStyleConfig( const StyleConfig& styleConfig ) { + mStyleConfig = styleConfig; + + setMaxNumVisibleItems( mStyleConfig.MaxNumVisibleItems ); + setPopUpToRoot( mStyleConfig.PopUpToRoot ); + setMenuWidthMode( mStyleConfig.menuWidthRule ); + return this; +} + +void UIDropDown::onWidgetClear( const Event* ) { + setText( "" ); + sendCommonEvent( Event::OnClear ); +} + +void UIDropDown::onItemKeyDown( const Event* Event ) { + const KeyEvent* KEvent = reinterpret_cast( Event ); + + if ( KEvent->getKeyCode() == KEY_RETURN ) + onItemClicked( Event ); + else if ( KEvent->getKeyCode() == KEY_ESCAPE ) { + hide(); + setFocus(); + } +} + +void UIDropDown::onPopUpFocusLoss( const Event* ) { + if ( NULL == getEventDispatcher() ) + return; + + bool frienIsFocus = NULL != mFriendNode && mFriendNode == getEventDispatcher()->getFocusNode(); + bool isChildFocus = isChild( getEventDispatcher()->getFocusNode() ); + + if ( getEventDispatcher()->getFocusNode() != this && !isChildFocus && !frienIsFocus ) { + hide(); + } +} + +void UIDropDown::onItemClicked( const Event* ) { + hide(); + setFocus(); +} + +void UIDropDown::onItemSelected( const Event* ) {} + +void UIDropDown::show() { + UIWidget* widget = getPopUpWidget(); + if ( NULL == widget ) + return; + + widget->setEnabled( true ); + widget->setVisible( true ); + + if ( NULL != getUISceneNode() && + getUISceneNode()->getUIThemeManager()->getDefaultEffectsEnabled() ) { + widget->runAction( Actions::Sequence::New( + Actions::Fade::New( 255.f == widget->getAlpha() ? 0.f : widget->getAlpha(), 255.f, + getUISceneNode()->getUIThemeManager()->getWidgetsFadeOutTime() ), + Actions::Spawn::New( Actions::Enable::New(), Actions::Visible::New( true ) ) ) ); + } +} + +void UIDropDown::hide() { + UIWidget* widget = getPopUpWidget(); + if ( NULL == widget ) + return; + + if ( NULL != getUISceneNode() && + getUISceneNode()->getUIThemeManager()->getDefaultEffectsEnabled() ) { + widget->runAction( Actions::Sequence::New( + Actions::FadeOut::New( getUISceneNode()->getUIThemeManager()->getWidgetsFadeOutTime() ), + Actions::Spawn::New( Actions::Disable::New(), Actions::Visible::New( false ) ) ) ); + } else { + widget->setEnabled( false ); + widget->setVisible( false ); + } +} + +Uint32 UIDropDown::onMouseOver( const Vector2i& position, const Uint32& flags ) { + if ( getParent()->isType( UI_TYPE_COMBOBOX ) ) { + return UITextInput::onMouseOver( position, flags ); + } else { + return UITextView::onMouseOver( position, flags ); + } +} + +Uint32 UIDropDown::onMouseLeave( const Vector2i& position, const Uint32& flags ) { + if ( getParent()->isType( UI_TYPE_COMBOBOX ) ) { + return UITextInput::onMouseLeave( position, flags ); + } else { + return UITextView::onMouseLeave( position, flags ); + } +} + +Uint32 UIDropDown::onKeyDown( const KeyEvent& Event ) { + return UITextInput::onKeyDown( Event ); +} + +bool UIDropDown::applyProperty( const StyleSheetProperty& attribute ) { + if ( !checkPropertyDefinition( attribute ) ) + return false; + + switch ( attribute.getPropertyDefinition()->getPropertyId() ) { + case PropertyId::PopUpToRoot: + setPopUpToRoot( attribute.asBool() ); + break; + case PropertyId::MaxVisibleItems: + setMaxNumVisibleItems( attribute.asUint() ); + break; + case PropertyId::MenuWidthMode: + setMenuWidthMode( menuWidthModeFromString( attribute.getValue() ) ); + break; + default: + return UITextInput::applyProperty( attribute ); + } + + return true; +} + +std::string UIDropDown::getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex ) const { + if ( NULL == propertyDef ) + return ""; + + switch ( propertyDef->getPropertyId() ) { + case PropertyId::PopUpToRoot: + return mStyleConfig.PopUpToRoot ? "true" : "false"; + case PropertyId::MaxVisibleItems: + return String::toString( mStyleConfig.MaxNumVisibleItems ); + case PropertyId::MenuWidthMode: + return menuWidthModeToString( mStyleConfig.menuWidthRule ); + default: + return UITextInput::getPropertyString( propertyDef, propertyIndex ); + } + + return ""; +} + +std::vector UIDropDown::getPropertiesImplemented() const { + auto props = UITextInput::getPropertiesImplemented(); + auto local = { PropertyId::PopUpToRoot, PropertyId::MaxVisibleItems, + PropertyId::MenuWidthMode }; + props.insert( props.end(), local.begin(), local.end() ); + return props; +} + +UIDropDown* UIDropDown::setMenuWidthMode( MenuWidthMode rule ) { + mStyleConfig.menuWidthRule = rule; + return this; +} + +UIDropDown::MenuWidthMode UIDropDown::getMenuWidthMode() const { + return mStyleConfig.menuWidthRule; +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/uidropdownlist.cpp b/src/eepp/ui/uidropdownlist.cpp index ba5c7fe21..cc6cab9e3 100644 --- a/src/eepp/ui/uidropdownlist.cpp +++ b/src/eepp/ui/uidropdownlist.cpp @@ -10,34 +10,6 @@ namespace EE { namespace UI { -UIDropDownList::MenuWidthMode UIDropDownList::menuWidthModeFromString( std::string_view str ) { - if ( "contents" == str || "fit-to-contents" == str ) - return MenuWidthMode::Contents; - if ( "contents-centered" == str || "fit-to-contents-centered" == str ) - return MenuWidthMode::ContentsCentered; - if ( "expand-if-needed" == str || "fit-to-drop-down-expand-if-needed" == str ) - return MenuWidthMode::ExpandIfNeeded; - if ( "expand-if-needed-centered" == str || "fit-to-drop-down-expand-if-needed-centered" == str ) - return MenuWidthMode::ExpandIfNeededCentered; - return MenuWidthMode::DropDown; // "dropdown" -} - -std::string UIDropDownList::menuWidthModeToString( MenuWidthMode rule ) { - switch ( rule ) { - case MenuWidthMode::DropDown: - return "dropdown"; - case MenuWidthMode::Contents: - return "contents"; - case MenuWidthMode::ContentsCentered: - return "contents-centered"; - case MenuWidthMode::ExpandIfNeeded: - return "expand-if-needed"; - case MenuWidthMode::ExpandIfNeededCentered: - return "expand-if-needed-centered"; - } - return "dropdown"; -} - UIDropDownList* UIDropDownList::NewWithTag( const std::string& tag ) { return eeNew( UIDropDownList, ( tag ) ); } @@ -46,15 +18,7 @@ UIDropDownList* UIDropDownList::New() { return eeNew( UIDropDownList, () ); } -UIDropDownList::UIDropDownList( const std::string& tag ) : - UITextInput( tag ), mListBox( NULL ), mFriendNode( NULL ) { - mEnabledCreateContextMenu = false; - setClipType( ClipType::ContentBox ); - setFlags( UI_AUTO_SIZE | UI_AUTO_PADDING | UI_SCROLLABLE ); - unsetFlags( UI_TEXT_SELECTION_ENABLED ); - - setAllowEditing( false ); - +UIDropDownList::UIDropDownList( const std::string& tag ) : UIDropDown( tag ), mListBox( NULL ) { applyDefaultTheme(); mListBox = UIListBox::NewWithTag( mTag + "::listbox" ); @@ -65,7 +29,7 @@ UIDropDownList::UIDropDownList( const std::string& tag ) : // This will force to change the parent when shown, and force the CSS style reload. mListBox->setParent( this ); - mListBox->on( Event::OnWidgetFocusLoss, [this]( auto event ) { onListBoxFocusLoss( event ); } ); + mListBox->on( Event::OnWidgetFocusLoss, [this]( auto event ) { onPopUpFocusLoss( event ); } ); mListBox->on( Event::OnItemSelected, [this]( auto event ) { onItemSelected( event ); } ); mListBox->on( Event::OnItemClicked, [this]( auto event ) { onItemClicked( event ); } ); mListBox->on( Event::OnItemKeyDown, [this]( auto event ) { onItemKeyDown( event ); } ); @@ -95,42 +59,11 @@ Uint32 UIDropDownList::getType() const { } bool UIDropDownList::isType( const Uint32& type ) const { - return UIDropDownList::getType() == type ? true : UITextInput::isType( type ); + return UIDropDownList::getType() == type ? true : UIDropDown::isType( type ); } -void UIDropDownList::setTheme( UITheme* Theme ) { - UIWidget::setTheme( Theme ); - - setThemeSkin( Theme, "dropdownlist" ); - - onThemeLoaded(); -} - -void UIDropDownList::onSizeChange() { - onAutoSize(); - - UITextInput::onSizeChange(); -} - -void UIDropDownList::onThemeLoaded() { - autoPadding(); - - onAutoSize(); -} - -void UIDropDownList::setFriendNode( UINode* friendNode ) { - mFriendNode = friendNode; -} - -void UIDropDownList::onAutoSize() { - Float max = eemax( PixelDensity::dpToPxI( getSkinSize().getHeight() ), - mTextCache.getLineSpacing() ); - - if ( mHeightPolicy == SizePolicy::WrapContent ) { - setInternalPixelsHeight( eeceil( max + mPaddingPx.Top + mPaddingPx.Bottom ) ); - } else if ( ( ( mFlags & UI_AUTO_SIZE ) || 0 == getSize().getHeight() ) && max > 0 ) { - setInternalPixelsHeight( eeceil( max ) ); - } +UIWidget* UIDropDownList::getPopUpWidget() const { + return mListBox; } UIListBox* UIDropDownList::getListBox() const { @@ -152,18 +85,14 @@ Uint32 UIDropDownList::onMouseUp( const Vector2i& Pos, const Uint32& Flags ) { } } - return UITextInput::onMouseUp( Pos, Flags ); + return UIDropDown::onMouseUp( Pos, Flags ); } -Uint32 UIDropDownList::onMouseClick( const Vector2i& Pos, const Uint32& Flags ) { - if ( ( Flags & EE_BUTTON_LMASK ) && NULL == mFriendNode ) - showList(); +Uint32 UIDropDownList::onKeyDown( const KeyEvent& Event ) { + if ( NULL != mListBox ) + mListBox->onKeyDown( Event ); - if ( NULL != mFriendNode ) { - UITextInput::onMouseClick( Pos, Flags ); - } - - return 1; + return UIDropDown::onKeyDown( Event ); } UIDropDownList* UIDropDownList::showList() { @@ -173,85 +102,30 @@ UIDropDownList* UIDropDownList::showList() { if ( !mListBox->isVisible() ) { if ( mListBox->getItemsCount() ) { Rectf tPadding = mListBox->getContainerPadding(); - Float sliderValue = mListBox->getVerticalScrollBar()->getValue(); - Float width = - NULL != mFriendNode ? mFriendNode->getSize().getWidth() : getSize().getWidth(); + Float contentsWidth = eeceil( PixelDensity::pxToDp( + mListBox->getMaxTextWidth() + + PixelDensity::dpToPx( mListBox->getContainerPadding().getWidth() ) + + mListBox->getReferenceItem()->getPixelsPadding().getWidth() + + mListBox->getVerticalScrollBar()->getPixelsSize().getWidth() ) ); - bool center = mStyleConfig.menuWidthRule == MenuWidthMode::ContentsCentered || - mStyleConfig.menuWidthRule == MenuWidthMode::ExpandIfNeededCentered; + Float width = getPopUpWidth( contentsWidth ); - Float contentsWidth = 0; - if ( mStyleConfig.menuWidthRule == MenuWidthMode::Contents || - mStyleConfig.menuWidthRule == MenuWidthMode::ContentsCentered || - mStyleConfig.menuWidthRule == MenuWidthMode::ExpandIfNeeded || - mStyleConfig.menuWidthRule == MenuWidthMode::ExpandIfNeededCentered ) { - contentsWidth = eeceil( PixelDensity::pxToDp( - mListBox->getMaxTextWidth() + - PixelDensity::dpToPx( mListBox->getContainerPadding().getWidth() ) + - mListBox->getReferenceItem()->getPixelsPadding().getWidth() + - mListBox->getVerticalScrollBar()->getPixelsSize().getWidth() ) ); - } - - if ( mStyleConfig.menuWidthRule == MenuWidthMode::Contents || - mStyleConfig.menuWidthRule == MenuWidthMode::ContentsCentered ) { - width = contentsWidth; - } - - if ( ( mStyleConfig.menuWidthRule == MenuWidthMode::ExpandIfNeeded || - mStyleConfig.menuWidthRule == MenuWidthMode::ExpandIfNeededCentered ) && - contentsWidth > width ) { - width = contentsWidth; - } - - mListBox->setSize( - width, (Int32)( eemin( mListBox->getItemsCount(), mStyleConfig.MaxNumVisibleItems ) * - mListBox->getRowHeight() ) + - tPadding.Top + tPadding.Bottom + - ( mListBox->getHorizontalScrollBar() && - mListBox->getHorizontalScrollBar()->isVisible() && - PixelDensity::dpToPx( width ) < mListBox->getMaxTextWidth() - ? mListBox->getHorizontalScrollBar()->getSize().getHeight() - : 0.f ) ); + Float height = + (Int32)( eemin( mListBox->getItemsCount(), mStyleConfig.MaxNumVisibleItems ) * + mListBox->getRowHeight() ) + + tPadding.Top + tPadding.Bottom + + ( mListBox->getHorizontalScrollBar() && + mListBox->getHorizontalScrollBar()->isVisible() && + PixelDensity::dpToPx( width ) < mListBox->getMaxTextWidth() + ? mListBox->getHorizontalScrollBar()->getSize().getHeight() + : 0.f ); + mListBox->setSize( width, height ); mListBox->getVerticalScrollBar()->setValue( sliderValue ); - if ( !mStyleConfig.PopUpToRoot ) - mListBox->setParent( getWindowContainer() ); - else - mListBox->setParent( getUISceneNode()->getRoot() ); - - mListBox->toFront(); - - Float offsetX = center ? eefloor( ( getSize().getWidth() - width ) * 0.5f ) : 0; - - Vector2f pos( mDpPos.x + offsetX, mDpPos.y + getSize().getHeight() ); - Vector2f posCpy( pos ); - nodeToWorld( posCpy ); - - if ( !getUISceneNode()->getWorldBounds().contains( - Rectf( posCpy, mListBox->getSize() ) ) ) { - pos = Vector2f( mDpPos.x + offsetX, mDpPos.y - mListBox->getSize().getHeight() ); - } - - if ( mStyleConfig.PopUpToRoot ) { - getParent()->nodeToWorld( pos ); - pos = PixelDensity::pxToDp( pos ); - } else { - Node* parentNode = getParent(); - Node* rp = getWindowContainer(); - while ( rp != parentNode ) { - pos += parentNode->getPosition(); - parentNode = parentNode->getParent(); - } - } - - mListBox->setPosition( pos ); - - show(); - - mListBox->setFocus(); + alignPopUp( mListBox ); } } else { hide(); @@ -259,19 +133,6 @@ UIDropDownList* UIDropDownList::showList() { return this; } -bool UIDropDownList::getPopUpToRoot() const { - return mStyleConfig.PopUpToRoot; -} - -UIDropDownList* UIDropDownList::setPopUpToRoot( bool popUpToRoot ) { - mStyleConfig.PopUpToRoot = popUpToRoot; - return this; -} - -Uint32 UIDropDownList::getMaxNumVisibleItems() const { - return mStyleConfig.MaxNumVisibleItems; -} - UIDropDownList* UIDropDownList::setMaxNumVisibleItems( const Uint32& maxNumVisibleItems ) { if ( maxNumVisibleItems != mStyleConfig.MaxNumVisibleItems ) { mStyleConfig.MaxNumVisibleItems = maxNumVisibleItems; @@ -284,52 +145,6 @@ UIDropDownList* UIDropDownList::setMaxNumVisibleItems( const Uint32& maxNumVisib return this; } -const UIDropDownList::StyleConfig& UIDropDownList::getStyleConfig() const { - return mStyleConfig; -} - -UIDropDownList* UIDropDownList::setStyleConfig( const StyleConfig& styleConfig ) { - mStyleConfig = styleConfig; - - setMaxNumVisibleItems( mStyleConfig.MaxNumVisibleItems ); - setPopUpToRoot( mStyleConfig.PopUpToRoot ); - setMenuWidthMode( mStyleConfig.menuWidthRule ); - return this; -} - -void UIDropDownList::onWidgetClear( const Event* ) { - setText( "" ); - sendCommonEvent( Event::OnClear ); -} - -void UIDropDownList::onItemKeyDown( const Event* Event ) { - const KeyEvent* KEvent = reinterpret_cast( Event ); - - if ( KEvent->getKeyCode() == KEY_RETURN ) - onItemClicked( Event ); - else if ( KEvent->getKeyCode() == KEY_ESCAPE ) { - hide(); - setFocus(); - } -} - -void UIDropDownList::onListBoxFocusLoss( const Event* ) { - if ( NULL == getEventDispatcher() ) - return; - - bool frienIsFocus = NULL != mFriendNode && mFriendNode == getEventDispatcher()->getFocusNode(); - bool isChildFocus = isChild( getEventDispatcher()->getFocusNode() ); - - if ( getEventDispatcher()->getFocusNode() != this && !isChildFocus && !frienIsFocus ) { - hide(); - } -} - -void UIDropDownList::onItemClicked( const Event* ) { - hide(); - setFocus(); -} - void UIDropDownList::onItemSelected( const Event* ) { setText( mListBox->getItemSelectedText() ); @@ -341,60 +156,6 @@ void UIDropDownList::onItemSelected( const Event* ) { sendCommonEvent( Event::OnSelectionChanged ); } -void UIDropDownList::show() { - if ( NULL == mListBox ) - return; - - mListBox->setEnabled( true ); - mListBox->setVisible( true ); - - if ( NULL != getUISceneNode() && - getUISceneNode()->getUIThemeManager()->getDefaultEffectsEnabled() ) { - mListBox->runAction( Actions::Sequence::New( - Actions::Fade::New( 255.f == mListBox->getAlpha() ? 0.f : mListBox->getAlpha(), 255.f, - getUISceneNode()->getUIThemeManager()->getWidgetsFadeOutTime() ), - Actions::Spawn::New( Actions::Enable::New(), Actions::Visible::New( true ) ) ) ); - } -} - -void UIDropDownList::hide() { - if ( NULL == mListBox ) - return; - - if ( NULL != getUISceneNode() && - getUISceneNode()->getUIThemeManager()->getDefaultEffectsEnabled() ) { - mListBox->runAction( Actions::Sequence::New( - Actions::FadeOut::New( getUISceneNode()->getUIThemeManager()->getWidgetsFadeOutTime() ), - Actions::Spawn::New( Actions::Disable::New(), Actions::Visible::New( false ) ) ) ); - } else { - mListBox->setEnabled( false ); - mListBox->setVisible( false ); - } -} - -Uint32 UIDropDownList::onMouseOver( const Vector2i& position, const Uint32& flags ) { - if ( getParent()->isType( UI_TYPE_COMBOBOX ) ) { - return UITextInput::onMouseOver( position, flags ); - } else { - return UITextView::onMouseOver( position, flags ); - } -} - -Uint32 UIDropDownList::onMouseLeave( const Vector2i& position, const Uint32& flags ) { - if ( getParent()->isType( UI_TYPE_COMBOBOX ) ) { - return UITextInput::onMouseLeave( position, flags ); - } else { - return UITextView::onMouseLeave( position, flags ); - } -} - -Uint32 UIDropDownList::onKeyDown( const KeyEvent& Event ) { - if ( NULL != mListBox ) - mListBox->onKeyDown( Event ); - - return UITextInput::onKeyDown( Event ); -} - void UIDropDownList::destroyListBox() { if ( !SceneManager::instance()->isShuttingDown() && NULL != mListBox && mListBox->getParent() != this ) { @@ -407,15 +168,6 @@ bool UIDropDownList::applyProperty( const StyleSheetProperty& attribute ) { return false; switch ( attribute.getPropertyDefinition()->getPropertyId() ) { - case PropertyId::PopUpToRoot: - setPopUpToRoot( attribute.asBool() ); - break; - case PropertyId::MaxVisibleItems: - setMaxNumVisibleItems( attribute.asUint() ); - break; - case PropertyId::MenuWidthMode: - setMenuWidthMode( menuWidthModeFromString( attribute.getValue() ) ); - break; case PropertyId::SelectedIndex: case PropertyId::SelectedText: case PropertyId::ScrollBarStyle: @@ -427,7 +179,7 @@ bool UIDropDownList::applyProperty( const StyleSheetProperty& attribute ) { else return false; default: - return UITextInput::applyProperty( attribute ); + return UIDropDown::applyProperty( attribute ); } return true; @@ -439,12 +191,6 @@ std::string UIDropDownList::getPropertyString( const PropertyDefinition* propert return ""; switch ( propertyDef->getPropertyId() ) { - case PropertyId::PopUpToRoot: - return mStyleConfig.PopUpToRoot ? "true" : "false"; - case PropertyId::MaxVisibleItems: - return String::toString( mStyleConfig.MaxNumVisibleItems ); - case PropertyId::MenuWidthMode: - return menuWidthModeToString( mStyleConfig.menuWidthRule ); case PropertyId::SelectedIndex: case PropertyId::SelectedText: case PropertyId::ScrollBarStyle: @@ -454,18 +200,16 @@ std::string UIDropDownList::getPropertyString( const PropertyDefinition* propert if ( NULL != mListBox ) return mListBox->getPropertyString( propertyDef, propertyIndex ); default: - return UITextInput::getPropertyString( propertyDef, propertyIndex ); + return UIDropDown::getPropertyString( propertyDef, propertyIndex ); } return ""; } std::vector UIDropDownList::getPropertiesImplemented() const { - auto props = UITextInput::getPropertiesImplemented(); - auto local = { - PropertyId::PopUpToRoot, PropertyId::MaxVisibleItems, PropertyId::SelectedIndex, - PropertyId::SelectedText, PropertyId::ScrollBarStyle, PropertyId::RowHeight, - PropertyId::VScrollMode, PropertyId::HScrollMode, PropertyId::MenuWidthMode }; + auto props = UIDropDown::getPropertiesImplemented(); + auto local = { PropertyId::SelectedIndex, PropertyId::SelectedText, PropertyId::ScrollBarStyle, + PropertyId::RowHeight, PropertyId::VScrollMode, PropertyId::HScrollMode }; props.insert( props.end(), local.begin(), local.end() ); return props; } @@ -476,7 +220,7 @@ void UIDropDownList::loadFromXmlNode( const pugi::xml_node& node ) { if ( NULL != mListBox ) mListBox->loadItemsFromXmlNode( node ); - UITextInput::loadFromXmlNode( node ); + UIDropDown::loadFromXmlNode( node ); endAttributesTransaction(); } @@ -486,13 +230,4 @@ void UIDropDownList::onClassChange() { mListBox->setClasses( getClasses() ); } -UIDropDownList* UIDropDownList::setMenuWidthMode( MenuWidthMode rule ) { - mStyleConfig.menuWidthRule = rule; - return this; -} - -UIDropDownList::MenuWidthMode UIDropDownList::getMenuWidthMode() const { - return mStyleConfig.menuWidthRule; -} - }} // namespace EE::UI diff --git a/src/eepp/ui/uidropdownmodellist.cpp b/src/eepp/ui/uidropdownmodellist.cpp new file mode 100644 index 000000000..9eed3e2b5 --- /dev/null +++ b/src/eepp/ui/uidropdownmodellist.cpp @@ -0,0 +1,267 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PUGIXML_HEADER_ONLY +#include + +namespace EE { namespace UI { + +UIDropDownModelList* UIDropDownModelList::NewWithTag( const std::string& tag ) { + return eeNew( UIDropDownModelList, ( tag ) ); +} + +UIDropDownModelList* UIDropDownModelList::New() { + return eeNew( UIDropDownModelList, () ); +} + +UIDropDownModelList::UIDropDownModelList( const std::string& tag ) : + UIDropDown( tag ), mListView( NULL ) { + mListView = static_cast( createDefaultListView() ); + mListView->setEnabled( false ); + mListView->setVisible( false ); + mListView->setParent( this ); + mListView->setSingleClickNavigation( true ); + + mListView->on( Event::OnWidgetFocusLoss, [this]( auto event ) { onPopUpFocusLoss( event ); } ); + mListView->on( Event::OnModelEvent, [this]( auto event ) { onItemSelected( event ); } ); + mListView->on( Event::KeyDown, [this]( auto event ) { onItemKeyDown( event ); } ); + mListView->on( Event::OnClear, [this]( auto event ) { onWidgetClear( event ); } ); + mListViewCloseCb = + mListView->on( Event::OnClose, [this]( const Event* ) { mListView = nullptr; } ); + + mListView->setOnSelectionChange( [this]() { + if ( !mListView->getSelection().isEmpty() ) { + updateSelectionIndex(); + sendCommonEvent( Event::OnSelectionChanged ); + } + } ); + + applyDefaultTheme(); +} + +UIDropDownModelList::~UIDropDownModelList() { + if ( mListView != nullptr && mListViewCloseCb ) + mListView->removeEventListener( mListViewCloseCb ); + destroyListView(); +} + +UIWidget* UIDropDownModelList::createDefaultListView() { + return UIListView::NewWithTag( mTag + "::listview" ); +} + +Uint32 UIDropDownModelList::getType() const { + return UI_TYPE_DROPDOWNMODELLIST; +} + +bool UIDropDownModelList::isType( const Uint32& type ) const { + return UIDropDownModelList::getType() == type ? true : UIDropDown::isType( type ); +} + +UIWidget* UIDropDownModelList::getPopUpWidget() const { + return mListView; +} + +UIAbstractTableView* UIDropDownModelList::getListView() const { + return mListView; +} + +void UIDropDownModelList::setListView( UIAbstractTableView* listView ) { + if ( listView == mListView ) + return; + + if ( mListView != nullptr ) { + if ( mListViewCloseCb ) + mListView->removeEventListener( mListViewCloseCb ); + mListView->close(); + } + + mListView = listView; + mListView->setEnabled( false ); + mListView->setVisible( false ); + mListView->setParent( this ); + + mListView->on( Event::OnWidgetFocusLoss, [this]( auto event ) { onPopUpFocusLoss( event ); } ); + mListView->on( Event::OnModelEvent, [this]( auto event ) { onItemSelected( event ); } ); + mListView->on( Event::KeyDown, [this]( auto event ) { onItemKeyDown( event ); } ); + mListView->on( Event::OnClear, [this]( auto event ) { onWidgetClear( event ); } ); + mListViewCloseCb = + mListView->on( Event::OnClose, [this]( const Event* ) { mListView = nullptr; } ); + + mListView->setOnSelectionChange( [this]() { + if ( !mListView->getSelection().isEmpty() ) { + sendCommonEvent( Event::OnSelectionChanged ); + } + } ); + + if ( mModel ) { + mListView->setModel( mModel ); + } +} + +std::shared_ptr UIDropDownModelList::getModel() const { + return mModel; +} + +void UIDropDownModelList::setModel( std::shared_ptr model ) { + mModel = model; + if ( mListView ) { + mListView->setModel( mModel ); + } +} + +Uint32 UIDropDownModelList::onMouseUp( const Vector2i& Pos, const Uint32& Flags ) { + if ( mEnabled && mVisible && isMouseOver() && NULL != mListView && mModel ) { + if ( Flags & EE_BUTTONS_WUWD ) { + if ( Flags & EE_BUTTON_WUMASK ) { + mListView->moveSelection( -1 ); + updateSelectionIndex(); + } else if ( Flags & EE_BUTTON_WDMASK ) { + if ( !mListView->getSelection().isEmpty() ) { + mListView->moveSelection( 1 ); + updateSelectionIndex(); + } else if ( mModel->hasChildren() ) { + mListView->getSelection().set( mModel->index( 0, 0 ) ); + updateSelectionIndex(); + } + } + } + } + + return UIDropDown::onMouseUp( Pos, Flags ); +} + +Uint32 UIDropDownModelList::onKeyDown( const KeyEvent& Event ) { + if ( NULL != mListView ) + mListView->onKeyDown( Event ); + + return UIDropDown::onKeyDown( Event ); +} + +UIDropDownModelList* UIDropDownModelList::showList() { + if ( NULL == mListView || NULL == mModel ) + return this; + + if ( !mListView->isVisible() ) { + if ( !mModel->hasChildren() ) + return this; + + Rectf tPadding = mListView->getPadding(); + + Float sliderValue = 0; + if ( mListView->getVerticalScrollBar() ) + sliderValue = mListView->getVerticalScrollBar()->getValue(); + + Float contentsWidth = eeceil( PixelDensity::pxToDp( + mListView->getMaxColumnContentWidth( 0, true ) + + PixelDensity::dpToPx( mListView->getPadding().getWidth() ) + + ( mListView->getVerticalScrollBar() + ? mListView->getVerticalScrollBar()->getPixelsSize().getWidth() + : 0.f ) ) ); + + Float width = getPopUpWidth( contentsWidth ); + + Float height = + (Int32)( eemin( (Uint32)mModel->rowCount(), mStyleConfig.MaxNumVisibleItems ) * + mListView->getRowHeight() ) + + tPadding.Top + tPadding.Bottom + mListView->getHeaderHeight() + + ( mListView->getHorizontalScrollBar() && + mListView->getHorizontalScrollBar()->isVisible() + ? mListView->getHorizontalScrollBar()->getSize().getHeight() + : 0.f ); + + mListView->setSize( width, height ); + + if ( mListView->getVerticalScrollBar() ) + mListView->getVerticalScrollBar()->setValue( sliderValue ); + + alignPopUp( mListView ); + } else { + hide(); + } + return this; +} + +UIDropDownModelList* +UIDropDownModelList::setMaxNumVisibleItems( const Uint32& maxNumVisibleItems ) { + if ( maxNumVisibleItems != mStyleConfig.MaxNumVisibleItems ) { + mStyleConfig.MaxNumVisibleItems = maxNumVisibleItems; + + if ( NULL != mListView && mModel ) + mListView->setSize( getSize().getWidth(), std::min( mStyleConfig.MaxNumVisibleItems, + (Uint32)mModel->rowCount() ) * + mListView->getRowHeight() ); + } + return this; +} + +void UIDropDownModelList::onItemClicked( const Event* ) { + updateSelectionIndex(); + hide(); + setFocus(); +} + +void UIDropDownModelList::updateSelectionIndex() { + if ( mListView->getSelection().isEmpty() ) + return; + ModelIndex idx = mListView->getSelection().first(); + if ( idx.isValid() ) { + Variant var = mModel->data( idx, ModelRole::Display ); + if ( var.isValid() && var.isString() ) + setText( var.toString() ); + sendCommonEvent( Event::OnItemSelected ); + sendCommonEvent( Event::OnValueChange ); + } +} + +void UIDropDownModelList::onItemSelected( const Event* event ) { + if ( event->getType() == Event::OnModelEvent ) { + const ModelEvent* modelEvent = static_cast( event ); + if ( modelEvent->getModelEventType() == ModelEventType::Open ) { + updateSelectionIndex(); + hide(); + setFocus(); + } + } +} + +void UIDropDownModelList::destroyListView() { + if ( !SceneManager::instance()->isShuttingDown() && NULL != mListView && + mListView->getParent() != this ) { + mListView->setParent( this ); + } +} + +std::string UIDropDownModelList::getPropertyString( const PropertyDefinition* propertyDef, + const Uint32& propertyIndex ) const { + if ( NULL == propertyDef ) + return ""; + + std::string res = UIDropDown::getPropertyString( propertyDef, propertyIndex ); + if ( res.empty() && NULL != mListView ) { + res = mListView->getPropertyString( propertyDef, propertyIndex ); + } + return res; +} + +std::vector UIDropDownModelList::getPropertiesImplemented() const { + auto props = UIDropDown::getPropertiesImplemented(); + if ( mListView ) { + auto listProps = mListView->getPropertiesImplemented(); + props.insert( props.end(), listProps.begin(), listProps.end() ); + } + return props; +} + +void UIDropDownModelList::onClassChange() { + if ( mListView ) + mListView->setClasses( getClasses() ); +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/uilistview.cpp b/src/eepp/ui/uilistview.cpp index b922b6e8b..5be3ae1a7 100644 --- a/src/eepp/ui/uilistview.cpp +++ b/src/eepp/ui/uilistview.cpp @@ -6,7 +6,11 @@ UIListView* UIListView::New() { return eeNew( UIListView, () ); } -UIListView::UIListView() : UITableView( "listview" ) { +UIListView* UIListView::NewWithTag( const std::string& tag ) { + return eeNew( UIListView, () ); +} + +UIListView::UIListView( const std::string& tag ) : UITableView( tag ) { setHeadersVisible( false ); setAutoExpandOnSingleColumn( true ); applyDefaultTheme(); diff --git a/src/eepp/ui/uithememanager.cpp b/src/eepp/ui/uithememanager.cpp index ff322973e..5e1a9bf14 100644 --- a/src/eepp/ui/uithememanager.cpp +++ b/src/eepp/ui/uithememanager.cpp @@ -14,8 +14,8 @@ UIThemeManager::UIThemeManager() : mThemeDefault( NULL ), mAutoApplyDefaultTheme( true ), mEnableDefaultEffects( false ), - mFadeInTime( Milliseconds( 100.f ) ), - mFadeOutTime( Milliseconds( 100.f ) ), + mFadeInTime( Milliseconds( 25.f ) ), + mFadeOutTime( Milliseconds( 25.f ) ), mTooltipTimeToShow( Milliseconds( 400 ) ), mTooltipFollowMouse( false ), mCursorSize( 16, 16 ) {} diff --git a/src/eepp/ui/uiwidgetcreator.cpp b/src/eepp/ui/uiwidgetcreator.cpp index c80b0c112..7c4a164d7 100644 --- a/src/eepp/ui/uiwidgetcreator.cpp +++ b/src/eepp/ui/uiwidgetcreator.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -67,6 +68,7 @@ void UIWidgetCreator::createBaseWidgetList() { registeredWidget["radiobutton"] = UIRadioButton::New; registeredWidget["combobox"] = UIComboBox::New; registeredWidget["dropdownlist"] = UIDropDownList::New; + registeredWidget["dropdownmodellist"] = UIDropDownModelList::New; registeredWidget["image"] = UIImage::New; registeredWidget["listbox"] = UIListBox::New; registeredWidget["menubar"] = UIMenuBar::New; diff --git a/src/examples/ui_dropdownmodellist/ui_dropdownmodellist.cpp b/src/examples/ui_dropdownmodellist/ui_dropdownmodellist.cpp new file mode 100644 index 000000000..48e1ad7c2 --- /dev/null +++ b/src/examples/ui_dropdownmodellist/ui_dropdownmodellist.cpp @@ -0,0 +1,51 @@ +#include +#include +#include + +using namespace EE::UI; +using namespace EE::UI::Models; + +EE_MAIN_FUNC int main( int, char** ) { + UIApplication app( { 640, 480, "eepp - UIDropDownModelList Example" } ); + app.getUI()->loadLayoutFromString( R"xml( + + + + + + )xml" ); + + if ( !app.getWindow()->isOpen() ) + return EXIT_FAILURE; + + UIDropDownModelList* dropDown = app.getUI()->find( "dropdown" ); + if ( dropDown ) { + std::vector options = { "Option 1: OpenGL", "Option 2: Vulkan", + "Option 3: Direct3D 11", "Option 4: Direct3D 12", + "Option 5: Metal", "Option 6: WebGL", + "Option 7: Software" }; + + auto model = ItemListOwnerModel::create( options ); + dropDown->setModel( model ); + dropDown->addEventListener( Event::OnItemSelected, []( const Event* event ) { + UIDropDownModelList* dropDown = event->getNode()->asType(); + ModelIndex index = dropDown->getListView()->getSelection().first(); + if ( index.isValid() ) { + String text = dropDown->getListView()->getModel()->data( index ).toString(); + Log::info( "Selected item index: %d, value: %s", (int)index.row(), text.c_str() ); + } + } ); + } + + return app.run(); +} diff --git a/src/tests/unit_tests/uidropdownmodellist.cpp b/src/tests/unit_tests/uidropdownmodellist.cpp new file mode 100644 index 000000000..97854e7a4 --- /dev/null +++ b/src/tests/unit_tests/uidropdownmodellist.cpp @@ -0,0 +1,43 @@ +#include "utest.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace EE; +using namespace EE::Window; +using namespace EE::Scene; +using namespace EE::UI; +using namespace EE::UI::Models; + +UTEST( UIDropDownModelList, basicFunctionality ) { + UIApplication app( + WindowSettings( 800, 600, "eepp - UIDropDownModelList Test", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1.5 ) ); + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + UISceneNode* sceneNode = app.getUI(); + + UIDropDownModelList* dropDown = UIDropDownModelList::New(); + dropDown->setParent( sceneNode ); + + std::vector items = { "Item 1", "Item 2", "Item 3" }; + auto model = ItemListOwnerModel::create( items ); + dropDown->setModel( model ); + + // Model should be set + EXPECT_TRUE( dropDown->getModel() == model ); + + // Items count should match + EXPECT_EQ( dropDown->getListView()->getModel()->rowCount(), 3ul ); + + // Max visible items + dropDown->setMaxNumVisibleItems( 2 ); + EXPECT_EQ( dropDown->getMaxNumVisibleItems(), 2ul ); +} diff --git a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp index ce6a0241b..7ff601c00 100644 --- a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp +++ b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp @@ -98,7 +98,10 @@ static std::map parseLLMProviders( const nlohmann::jso model.cacheConfiguration = cache; } - provider.models.push_back( model ); + model.hash = hashCombine( std::hash()( model.name ), + std::hash()( model.provider ) ); + + provider.models.emplace_back( std::move( model ) ); } } diff --git a/src/tools/ecode/plugins/aiassistant/chatui.cpp b/src/tools/ecode/plugins/aiassistant/chatui.cpp index f45b43f7e..a3925f1fc 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.cpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.cpp @@ -30,6 +30,107 @@ using namespace EE::Window; namespace ecode { +class LLMModelsModel : public Model { + public: + enum Columns { Name, Provider, Hash }; + + LLMModelsModel( const std::vector& models, UISceneNode* uiSceneNode = nullptr ) : + mModels( models ), mUISceneNode( uiSceneNode ) { + mCurModels.reserve( mModels.size() ); + for ( const auto& model : mModels ) { + mCurModels.emplace_back( &model ); + } + } + + virtual size_t rowCount( const ModelIndex& = ModelIndex() ) const override { + return mCurModels.size(); + } + + virtual size_t columnCount( const ModelIndex& = ModelIndex() ) const override { return 2; } + + virtual std::string columnName( const size_t& column ) const override { + switch ( column ) { + case Columns::Name: + return mUISceneNode ? mUISceneNode->i18n( "name", "Name" ) : "Name"; + case Columns::Provider: + return mUISceneNode ? mUISceneNode->i18n( "provider", "Provider" ) : "Provider"; + case Columns::Hash: + return mUISceneNode ? mUISceneNode->i18n( "hash", "Hash" ) : "Hash"; + } + return ""; + } + + virtual Variant data( const ModelIndex& index, + ModelRole role = ModelRole::Display ) const override { + if ( role != ModelRole::Display ) + return {}; + + if ( index.row() < 0 || static_cast( index.row() ) >= mCurModels.size() ) + return {}; + + const auto& model = *mCurModels[index.row()]; + + switch ( index.column() ) { + case Columns::Name: { + if ( model.displayName.has_value() && !model.displayName->empty() ) { + return Variant( model.displayName->c_str() ); + } + return Variant( model.name.c_str() ); + } + case Columns::Provider: { + return Variant( model.provider.c_str() ); + } + case Columns::Hash: { + return Variant( static_cast( model.hash ) ); + } + } + + return {}; + } + + ModelIndex getFromHash( Uint64 hash ) { + auto it = std::find_if( mCurModels.begin(), mCurModels.end(), + [hash]( const LLMModel* model ) { return hash == model->hash; } ); + return it != mCurModels.end() ? index( std::distance( mCurModels.begin(), it ) ) + : index( 0 ); + } + + void setFilter( const std::string& filter ) { + if ( mCurFilter == filter ) + return; + mCurFilter = filter; + mCurModels.clear(); + + for ( const auto& model : mModels ) { + if ( filter.empty() ) { + mCurModels.emplace_back( &model ); + continue; + } + + bool matchesName = String::icontains( model.name, filter ); + bool matchesDisplayName = + model.displayName.has_value() && String::icontains( *model.displayName, filter ); + bool matchesProvider = String::icontains( model.provider, filter ); + + if ( matchesName || matchesDisplayName || matchesProvider ) { + mCurModels.emplace_back( &model ); + } + } + + invalidate(); + } + + const std::vector& getCurModels() const { return mCurModels; } + + void refresh() { setFilter( mCurFilter ); } + + protected: + const std::vector& mModels; + std::vector mCurModels; + std::string mCurFilter; + UISceneNode* mUISceneNode; +}; + static const char* DEFAULT_PROVIDER = "google"; static const char* DEFAULT_MODEL = "gemini-2.5-flash"; @@ -71,13 +172,7 @@ LLMChat::Role LLMChat::stringToRole( UIPushButton* userBut ) { static const char* DEFAULT_LAYOUT = R"xml( @@ -172,17 +291,17 @@ DropDownList.role_ui { + + + + - - - - + - @@ -219,10 +338,7 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : ->asType(); mChatsList = findByClass( "llm_chats" ); - mModelDDL = findByClass( "model_ui" ); - - // mRefreshModels = find( "refresh_model_ui" ); - // mRefreshModels->onClick( [this]( auto ) { execute( "ai-refresh-local-models" ); } ); + mModelBtn = findByClass( "model_ui" ); mChatMore = find( "llm_more" ); mChatMore->onClick( [this]( auto ) { execute( "ai-show-menu" ); } ); @@ -276,10 +392,6 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : mChatUserRole = find( "llm_user" ); mChatUserRole->onClick( [this]( auto ) { execute( "ai-chat-toggle-role" ); } ); - /* mChatPrivate = find( "llm_private_chat" ); - mChatPrivate->on( Event::OnValueChange, - [this]( auto ) { mChatIsPrivate = mChatPrivate->isSelected(); } ); */ - auto setCmd = [this]( const std::string& name, const CommandCallback& cb ) { setCommand( name, cb ); mChatInput->getDocument().setCommand( name, cb ); @@ -387,9 +499,26 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : mLocateInput->getDocument().selectAll(); } ); + setCmd( "ai-select-model", [this] { + if ( !mLocateModelBarLayout->isVisible() ) { + if ( mLocateModelTable->getModel() ) { + mLocateModelInput->setText( "" ); + static_cast( mLocateModelTable->getModel() )->setFilter( "" ); + } + + showSelectModel(); + + mLocateModelTable->runOnMainThread( [this] { + auto model = static_cast( mLocateModelTable->getModel() ); + if ( model ) + mLocateModelTable->setSelection( model->getFromHash( mCurModel.hash ) ); + } ); + } else + hideSelectModel(); + } ); + setCmd( "ai-toggle-private-chat", [this] { mChatIsPrivate = !mChatIsPrivate; - /* mChatPrivate->toggleSelection(); */ if ( mChatIsPrivate ) mChatInput->addClass( "incognito" ); @@ -533,7 +662,7 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : } ); } ); - setCmd( "ai-refresh-local-models", [this] { fillApiModels( mModelDDL ); } ); + setCmd( "ai-refresh-local-models", [this] { fillApiModels(); } ); mChatHistory = find( "llm_chat_history" ); mChatHistory->onClick( [this]( auto ) { showChatHistory(); } ); @@ -541,10 +670,13 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : mChatAttach = find( "llm_attach" ); mChatAttach->onClick( [this]( auto ) { execute( "ai-show-add-context-menu" ); } ); + mModelBtn->onClick( [this]( auto ) { execute( "ai-select-model" ); } ); + if ( getPlugin() == nullptr ) return; initAttachFile(); + initSelectModel(); auto providers = getPlugin()->getProviders(); setProviders( std::move( providers ) ); @@ -566,7 +698,7 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : } } - fillModelDropDownList( mModelDDL ); + fillModelDropDownList(); const auto appendShortcutToTooltip = [this]( UIPushButton* but, const std::string& cmd ) { auto kb = getKeyBindings().getCommandKeybindString( cmd ); @@ -583,10 +715,9 @@ LLMChatUI::LLMChatUI( PluginManager* manager ) : appendShortcutToTooltip( mChatStop, "ai-prompt" ); appendShortcutToTooltip( mChatAdd, "ai-add-chat" ); appendShortcutToTooltip( mChatSettings, "ai-settings" ); - // appendShortcutToTooltip( mChatPrivate, "ai-toggle-private-chat" ); appendShortcutToTooltip( mChatMore, "ai-show-menu" ); appendShortcutToTooltip( mChatUserRole, "ai-chat-toggle-role" ); - // appendShortcutToTooltip( mRefreshModels, "ai-refresh-local-models" ); + appendShortcutToTooltip( mModelBtn, "ai-select-model" ); addKb( mChatInput, "mod+keypad enter", "ai-prompt", true, false ); addKb( mChatInput, "mod+shift+keypad enter", "ai-add-chat", true, false ); @@ -643,6 +774,7 @@ void LLMChatUI::bindCmds( UICodeEditor* editor, bool bindToChatUI ) { addKb( editor, "mod+shift+l", "ai-refresh-local-models", bindToChatUI ); addKb( editor, "mod+shift+a", "ai-attach-file", bindToChatUI ); addKb( editor, "mod+shift+z", "ai-link-file", bindToChatUI ); + addKb( editor, "mod+shift+x", "ai-select-model", bindToChatUI ); if ( bindToChatUI ) addKb( editor, "mod+shift+return", "ai-add-chat", bindToChatUI ); @@ -663,6 +795,14 @@ std::optional LLMChatUI::getModel( const std::string& provider, return {}; } +std::optional LLMChatUI::getModel( Uint64 hash ) { + auto modelIt = std::find_if( mModels.begin(), mModels.end(), + [hash]( const LLMModel& model ) { return hash == model.hash; } ); + if ( modelIt != mModels.end() ) + return *modelIt; + return {}; +} + void LLMChatUI::showChatHistory() { auto plugin = getPlugin(); if ( plugin == nullptr ) @@ -832,8 +972,10 @@ void LLMChatUI::showChatHistory() { } ); } -void LLMChatUI::fillApiModels( UIDropDownList* modelDDL ) { +void LLMChatUI::fillApiModels() { mPendingModelsToLoad = 0; + mNewModels.clear(); + for ( auto& [name, data] : mProviders ) { if ( !data.enabled || !data.fetchModelsUrl ) continue; @@ -869,6 +1011,8 @@ void LLMChatUI::fillApiModels( UIDropDownList* modelDDL ) { model.displayName = el.value( "display_name", "" ); model.isEphemeral = true; + model.hash = hashCombine( std::hash()( model.name ), + std::hash()( model.provider ) ); if ( model.name.empty() ) continue; @@ -877,39 +1021,25 @@ void LLMChatUI::fillApiModels( UIDropDownList* modelDDL ) { model.maxOutputTokens = el.value( "max_context_length", 0 ); data.models.emplace_back( model ); + mNewModels.push_back( model ); } mPendingModelsToLoad++; - std::string pname = name; - modelDDL->runOnMainThread( [pname = std::move( pname ), modelDDL, this] { - String providerName( pname ); - std::vector removeValues; - size_t count = modelDDL->getListBox()->getItemsCount(); - for ( size_t i = 0; i < count; i++ ) { - const String& txt = modelDDL->getListBox()->getItemText( i ); - if ( txt.contains( providerName ) ) - removeValues.emplace_back( txt ); - } - - for ( const auto& val : removeValues ) - modelDDL->getListBox()->removeListBoxItem( val ); - - const auto& models = mProviders[pname].models; - std::vector newModels; - for ( const auto& model : models ) { - if ( !model.isEphemeral ) - continue; - newModels.emplace_back( String::format( "%s (%s)", model.name, pname ) ); - mModelsMap[newModels[newModels.size() - 1].getHash()] = model; - } - - modelDDL->getListBox()->addListBoxItems( newModels ); - + runOnMainThread( [this] { mPendingModelsToLoad--; + if ( mPendingModelsToLoad == 0 ) { + mModels.erase( + std::remove_if( mModels.begin(), mModels.end(), + []( const LLMModel& model ) { return model.isEphemeral; } ), + mModels.end() ); - if ( mPendingModelsToLoad == 0 ) + mModels.insert( mModels.end(), mNewModels.begin(), mNewModels.end() ); + + if ( mLocateModelTable && mLocateModelTable->getModel() ) + loadSelectModel(); onInit(); + } } ); } @@ -926,46 +1056,28 @@ String LLMChatUI::getModelDisplayName( const LLMModel& model ) const { data.displayName ? *data.displayName : String::capitalize( data.name ) ); } -bool LLMChatUI::selectModel( UIDropDownList* modelDDL, const LLMModel& model ) { - auto modelName = getModelDisplayName( model ); - auto index = modelDDL->getListBox()->getItemIndex( modelName ); - if ( index != eeINDEX_NOT_FOUND ) { - modelDDL->getListBox()->setSelected( index ); +bool LLMChatUI::selectModel( std::optional model ) { + if ( model ) { + mModelBtn->setText( getModelDisplayName( *model ) ); + mCurModel = *model; return true; } return false; } -void LLMChatUI::fillModelDropDownList( UIDropDownList* modelDDL ) { - std::vector models; - std::size_t selectedIndex = 0; +void LLMChatUI::fillModelDropDownList() { + mModels.clear(); + std::size_t reserve = 0; + for ( const auto& [_, data] : mProviders ) + reserve += data.models.size(); + mModels.reserve( reserve + 8 /* extra space for local models */ ); for ( const auto& [name, data] : mProviders ) { if ( !data.enabled ) continue; - - for ( const auto& model : data.models ) { - String modelName( String::format( - "%s (%s)", model.displayName ? *model.displayName : model.name, - data.displayName ? *data.displayName : String::capitalize( data.name ) ) ); - mModelsMap[modelName.getHash()] = model; - if ( model.provider == mCurModel.provider && model.name == mCurModel.name ) - selectedIndex = models.size(); - models.push_back( std::move( modelName ) ); - } + for ( const auto& model : data.models ) + mModels.push_back( model ); } - modelDDL->getListBox()->clear(); - modelDDL->getListBox()->addListBoxItems( std::move( models ) ); - modelDDL->getListBox()->setSelected( selectedIndex ); - modelDDL->on( Event::OnValueChange, [this, modelDDL]( auto ) { - auto selectedModel = - mModelsMap.find( modelDDL->getListBox()->getItemSelectedText().getHash() ); - if ( selectedModel != mModelsMap.end() ) { - mCurModel = selectedModel->second; - } - } ); - - modelDDL->getUISceneNode()->getThreadPool()->run( - [this, modelDDL] { fillApiModels( modelDDL ); } ); + getUISceneNode()->getThreadPool()->run( [this] { fillApiModels(); } ); } void LLMChatUI::resizeToFit( UICodeEditor* editor ) { @@ -1077,8 +1189,8 @@ std::string LLMChatUI::unserialize( const nlohmann::json& payload ) { if ( mCurModel.name.empty() ) return payload.value( "input", "" ); - if ( !selectModel( mModelDDL, mCurModel ) ) - fillModelDropDownList( mModelDDL ); + if ( !selectModel( mCurModel ) ) + fillModelDropDownList(); if ( payload.contains( "chat" ) && payload["chat"].is_object() ) { const auto& chat = payload["chat"]; @@ -1488,10 +1600,10 @@ const LLMModel& LLMChatUI::getCheapestModelFromCurrentProvider() const { } void LLMChatUI::onInit() { - if ( !mModelDDL ) + if ( !mModelBtn ) return; - if ( getModelDisplayName( mCurModel ) != mModelDDL->getListBox()->getItemSelectedText() ) - selectModel( mModelDDL, mCurModel ); + if ( getModelDisplayName( mCurModel ) != mModelBtn->getText() ) + selectModel( mCurModel ); } void LLMChatUI::updateTabTitle() { @@ -1549,9 +1661,12 @@ void LLMChatUI::updateLocateBarColumns() { mLocateTable->setColumnWidth( 1, width - mLocateTable->getColumnWidth( 0 ) ); } +// File picker + void LLMChatUI::showAttachFile() { if ( getPlugin() == nullptr ) return; + hideSelectModel(); auto text = mLocateInput->getText(); auto ctx = getPlugin()->getPluginContext(); if ( !ctx->isDirTreeReady() ) { @@ -1687,4 +1802,90 @@ void LLMChatUI::initAttachFile() { } ); } +// Model Picker + +void LLMChatUI::updateLocateModelBarColumns() { + Float width = eeceil( mLocateModelTable->getPixelsSize().getWidth() ); + width -= mLocateModelTable->getVerticalScrollBar()->getPixelsSize().getWidth(); + mLocateModelTable->setColumnsVisible( { 0, 1 } ); + mLocateModelTable->setColumnWidth( 0, eeceil( width * 0.8 ) ); + mLocateModelTable->setColumnWidth( 1, width - mLocateModelTable->getColumnWidth( 0 ) ); +} + +void LLMChatUI::loadSelectModel() { + auto ctx = getPlugin()->getPluginContext(); + mLocateModelTable->setModel( + std::make_shared( mModels, ctx->getUISceneNode() ) ); + + static_cast( mLocateModelTable->getModel() ) + ->setFilter( mLocateModelInput->getText() ); +} + +void LLMChatUI::showSelectModel() { + if ( getPlugin() == nullptr ) + return; + hideAttachFile(); + + if ( nullptr == mLocateModelTable->getModel() ) + loadSelectModel(); + + static_cast( mLocateModelTable->getModel() ) + ->setFilter( mLocateModelInput->getText() ); + + mLocateModelBarLayout->setVisible( true ); + mLocateModelInput->setFocus(); + updateLocateModelBarColumns(); +} + +void LLMChatUI::hideSelectModel() { + mLocateModelBarLayout->setVisible( false ); +} + +void LLMChatUI::initSelectModel() { + mLocateModelBarLayout = findByClass( "llm_chat_select_model" ); + mLocateModelInput = findByClass( "llm_chat_select_model_input" ); + mLocateModelTable = findByClass( "llm_chat_model_locate" ); + mLocateModelTable->setHeadersVisible( false ); + + mLocateModelTable->on( Event::OnSizeChange, + [this]( const Event* ) { updateLocateModelBarColumns(); } ); + + mLocateModelInput->on( Event::OnTextChanged, [this]( const Event* ) { + showSelectModel(); + updateLocateModelBarColumns(); + } ); + mLocateModelInput->on( Event::OnPressEnter, [this]( const Event* ) { + KeyEvent keyEvent( mLocateModelTable, Event::KeyDown, KEY_RETURN, SCANCODE_UNKNOWN, 0, 0 ); + mLocateModelTable->forceKeyDown( keyEvent ); + } ); + mLocateModelInput->on( Event::KeyDown, [this]( const Event* event ) { + const KeyEvent* keyEvent = static_cast( event ); + mLocateModelTable->forceKeyDown( *keyEvent ); + } ); + mLocateModelBarLayout->setCommand( "close-locatebar", [this] { + hideSelectModel(); + if ( mChatInput ) + mChatInput->setFocus(); + } ); + mLocateModelBarLayout->getKeyBindings().addKeybindsString( { + { "escape", "close-locatebar" }, + } ); + mLocateModelTable->on( Event::KeyDown, [this]( const Event* event ) { + const KeyEvent* keyEvent = static_cast( event ); + if ( keyEvent->getKeyCode() == KEY_ESCAPE ) + mLocateModelBarLayout->execute( "close-locatebar" ); + } ); + mLocateModelTable->on( Event::OnModelEvent, [this]( const Event* event ) { + const ModelEvent* modelEvent = static_cast( event ); + if ( modelEvent->getModelEventType() == ModelEventType::Open ) { + Variant vHash( modelEvent->getModel()->data( + modelEvent->getModel()->index( modelEvent->getModelIndex().row(), + LLMModelsModel::Hash ), + ModelRole::Display ) ); + selectModel( getModel( vHash.asUint64() ) ); + mLocateModelBarLayout->execute( "close-locatebar" ); + } + } ); +} + } // namespace ecode diff --git a/src/tools/ecode/plugins/aiassistant/chatui.hpp b/src/tools/ecode/plugins/aiassistant/chatui.hpp index f8e5edec6..0f3c2c73f 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.hpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.hpp @@ -110,20 +110,28 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter { UIPushButton* mChatAttach{ nullptr }; UISelectButton* mChatPrivate{ nullptr }; UIScrollView* mChatScrollView{ nullptr }; - UIDropDownList* mModelDDL{ nullptr }; + UIPushButton* mModelBtn{ nullptr }; + + // Locate file UIVLinearLayoutCommandExecuter* mLocateBarLayout{ nullptr }; UITextInput* mLocateInput{ nullptr }; UITableView* mLocateTable{ nullptr }; + // Select model + UIVLinearLayoutCommandExecuter* mLocateModelBarLayout{ nullptr }; + UITextInput* mLocateModelInput{ nullptr }; + UITableView* mLocateModelTable{ nullptr }; + std::unique_ptr mRequest; std::unique_ptr mSummaryRequest; LLMProviders mProviders; LLMModel mCurModel; - std::unordered_map mModelsMap; + std::vector mModels; int mPendingModelsToLoad{ 0 }; bool mChatIsPrivate{ false }; bool mChatLocked{ false }; bool mLinkMode{ false }; + std::vector mNewModels; LLMModel findModel( const std::string& provider, const std::string& model ); @@ -151,13 +159,13 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter { UIWidget* addChatUI( LLMChat::Role role ); - void fillApiModels( UIDropDownList* modelDDL ); + void fillApiModels(); String getModelDisplayName( const LLMModel& model ) const; - bool selectModel( UIDropDownList* modelDDL, const LLMModel& model ); + bool selectModel( std::optional model ); - void fillModelDropDownList( UIDropDownList* modelDDL ); + void fillModelDropDownList(); void resizeToFit( UICodeEditor* editor ); @@ -173,6 +181,8 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter { std::optional getModel( const std::string& provider, const std::string& modelName ); + std::optional getModel( Uint64 hash ); + void saveChat(); void onInit(); @@ -188,12 +198,22 @@ class LLMChatUI : public UILinearLayout, public WidgetCommandExecuter { void initAttachFile(); + void initSelectModel(); + void updateLocateBarColumns(); void showAttachFile(); void hideAttachFile(); + void updateLocateModelBarColumns(); + + void loadSelectModel(); + + void showSelectModel(); + + void hideSelectModel(); + void insertFileToDocument( std::string path, std::shared_ptr cdoc ); void replaceFileLinksToContents( std::string& text ); diff --git a/src/tools/ecode/plugins/aiassistant/protocol.hpp b/src/tools/ecode/plugins/aiassistant/protocol.hpp index 0382f4add..9166bf2f2 100644 --- a/src/tools/ecode/plugins/aiassistant/protocol.hpp +++ b/src/tools/ecode/plugins/aiassistant/protocol.hpp @@ -14,6 +14,7 @@ struct LLMCacheConfiguration { }; struct LLMModel { + std::size_t hash{ 0 }; std::string name; std::string provider; std::optional displayName; diff --git a/src/tools/ecode/statusbuildoutputcontroller.cpp b/src/tools/ecode/statusbuildoutputcontroller.cpp index 827bed229..aa0eefb4c 100644 --- a/src/tools/ecode/statusbuildoutputcontroller.cpp +++ b/src/tools/ecode/statusbuildoutputcontroller.cpp @@ -217,16 +217,20 @@ void StatusBuildOutputController::runBuild( const std::string& buildName, } if ( enableBuildButton && buildButton ) - buildButton->setEnabled( true ); + buildButton->ensureMainThread( [buildButton] { buildButton->setEnabled( true ); } ); if ( enableCleanButton && cleanButton ) - cleanButton->runOnMainThread( [cleanButton] { cleanButton->setEnabled( true ); } ); + cleanButton->ensureMainThread( [cleanButton] { cleanButton->setEnabled( true ); } ); - if ( buildAndRunButton ) - buildAndRunButton->setEnabled( true ); + if ( buildAndRunButton ) { + buildAndRunButton->ensureMainThread( + [buildAndRunButton] { buildAndRunButton->setEnabled( true ); } ); + } - mBuildButton->setEnabled( true ); - mStopButton->setEnabled( false ); + mBuildButton->ensureMainThread( [this] { + mBuildButton->setEnabled( true ); + mStopButton->setEnabled( false ); + } ); }; auto res = pbm->build(