diff --git a/bin/assets/i18n/en.xml b/bin/assets/i18n/en.xml
index 2ee79ecb0..586295b0e 100644
--- a/bin/assets/i18n/en.xml
+++ b/bin/assets/i18n/en.xml
@@ -887,14 +887,14 @@ the directory tree and in file dialogs to open a folder or file.
Type
Type to Locate
UI Font & Size...
- Ui Font Size
+ UI Font Size
UI Language
Multisample Anti-Aliasing Level
- Ui Panel Font Size
+ UI Panel Font Size
UI Prefers Color Scheme
Renderer
Renderer Version
- Ui Scale Factor
+ UI Scale Factor
UI Theme
Copy
Copy Containing Folder Path
diff --git a/bin/assets/i18n/zh.xml b/bin/assets/i18n/zh.xml
index 575b64cff..de3b2502c 100644
--- a/bin/assets/i18n/zh.xml
+++ b/bin/assets/i18n/zh.xml
@@ -681,7 +681,7 @@ file in the directory tree.
Type
Type to Locate
界面字体和大小...
- Ui字体大小
+ UI字体大小
界面语言
多重采样抗锯齿级别
界面面板字体大小
diff --git a/include/eepp/ui.hpp b/include/eepp/ui.hpp
index 12b7e0186..9ad7b75f0 100644
--- a/include/eepp/ui.hpp
+++ b/include/eepp/ui.hpp
@@ -93,6 +93,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/include/eepp/ui/tools/uisettingspanel.hpp b/include/eepp/ui/tools/uisettingspanel.hpp
new file mode 100644
index 000000000..a11d9122c
--- /dev/null
+++ b/include/eepp/ui/tools/uisettingspanel.hpp
@@ -0,0 +1,221 @@
+#ifndef EE_UI_TOOLS_UISETTINGSPANEL_HPP
+#define EE_UI_TOOLS_UISETTINGSPANEL_HPP
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace pugi {
+class xml_node;
+}
+
+namespace EE::UI {
+class UICheckBox;
+}
+
+namespace EE::UI::Tools {
+
+struct SettingView;
+
+struct EE_API SettingDescriptor {
+ std::string id;
+ std::string category;
+ String name;
+ String description;
+ String group;
+};
+
+struct EE_API BoolPointerSetting {
+ bool* value{ nullptr };
+ std::function apply;
+};
+
+struct EE_API BoolSetting {
+ std::function get;
+ std::function set;
+};
+
+struct EE_API ChoiceSetting {
+ std::vector choices;
+ std::vector descriptions;
+ std::function get;
+ std::function set;
+};
+
+struct EE_API EditableChoiceSetting {
+ std::vector choices;
+ std::function get;
+ std::function set;
+};
+
+struct EE_API IntegerSetting {
+ int min{ 0 };
+ int max{ 0 };
+ std::function get;
+ std::function set;
+};
+
+struct EE_API TextSetting {
+ std::function get;
+ std::function set;
+ bool commitOnFocusLoss{ false };
+ bool password{ false };
+};
+
+struct EE_API FloatSetting {
+ double min{ 0 };
+ double max{ 0 };
+ double step{ 0 };
+ std::function get;
+ std::function set;
+};
+
+struct EE_API ActionSetting {
+ String buttonText;
+ std::function action;
+};
+
+using SettingValue =
+ std::variant;
+
+struct EE_API SettingDefinition {
+ SettingDescriptor descriptor;
+ SettingValue value;
+ bool enabled{ true };
+};
+
+struct EE_API SettingsCategory {
+ std::string id;
+ String parent;
+ String name;
+};
+
+struct EE_API SettingsGroup {
+ std::string category;
+ String name;
+ size_t beforeSetting{ 0 };
+};
+
+class EE_API SettingsModel {
+ public:
+ void clear();
+
+ bool addCategory( SettingsCategory category );
+
+ bool addGroup( SettingsGroup group );
+
+ bool addSetting( SettingDefinition setting );
+
+ bool hasCategory( const std::string& id ) const;
+
+ const std::vector& categories() const { return mCategories; }
+
+ const std::vector& groups() const { return mGroups; }
+
+ std::vector& settings() { return mSettings; }
+
+ const std::vector& settings() const { return mSettings; }
+
+ private:
+ std::vector mCategories;
+ std::vector mGroups;
+ std::vector mSettings;
+};
+
+class EE_API UISettingsPanel : public UILinearLayout {
+ public:
+ static UISettingsPanel* New( UIWidget* parent );
+
+ virtual ~UISettingsPanel();
+
+ SettingsModel& getModel();
+
+ const SettingsModel& getModel() const;
+
+ bool addCategory( std::string id, String parent, String name );
+
+ bool addGroup( std::string category, String name );
+
+ bool addBool( SettingDescriptor descriptor, bool* value,
+ std::function apply = {} );
+
+ bool addBool( SettingDescriptor descriptor, std::function get,
+ std::function set );
+
+ bool addChoice( SettingDescriptor descriptor, std::vector choices,
+ std::function get, std::function set,
+ std::vector choiceDescriptions = {} );
+
+ bool addEditableChoice( SettingDescriptor descriptor, std::vector choices,
+ std::function get, std::function set );
+
+ bool addInteger( SettingDescriptor descriptor, int min, int max, std::function get,
+ std::function set );
+
+ bool addText( SettingDescriptor descriptor, std::function get,
+ std::function set, bool commitOnFocusLoss = false );
+
+ bool addFloat( SettingDescriptor descriptor, double min, double max, double step,
+ std::function get, std::function set );
+
+ bool addAction( SettingDescriptor descriptor, String buttonText, std::function action );
+
+ void build();
+
+ void selectCategory( const std::string& category );
+
+ void setCategoryEnabled( const std::string& category, bool enabled,
+ const std::string& excludedSetting = {} );
+
+ void refreshTextSetting( const std::string& id );
+
+ void setSearchResultsText( String text );
+
+ void setFilter( String filter );
+
+ void focusSearch();
+
+ void focusCategories();
+
+ bool isBuilt() const;
+
+ protected:
+ struct Impl;
+ std::unique_ptr mImpl;
+
+ explicit UISettingsPanel( UIWidget* parent );
+
+ void selectCategory( Impl& panel, const std::string& category );
+
+ void addCategory( Impl& panel, const std::string& id, const String& parent,
+ const String& name );
+
+ void addSubcategoryHeading( Impl& panel, const std::string& category, const String& name );
+
+ void setupCategories( Impl& panel );
+
+ UIWidget* createRow( Impl& panel, SettingDefinition& setting, SettingView& view,
+ pugi::xml_node layout );
+
+ UICheckBox* createBoolControl( Impl& panel, SettingDefinition& setting, SettingView& view );
+
+ void materializeCategory( Impl& panel, const std::string& category );
+
+ void materializeVisibleSettings( Impl& panel, const String& query );
+
+ void setCategoryEnabled( Impl& panel, const std::string& category, bool enabled,
+ const std::string& excludedSetting = {} );
+
+ void refreshTextSetting( Impl& panel, const std::string& id );
+
+ void filter( Impl& panel );
+};
+
+} // namespace EE::UI::Tools
+
+#endif
diff --git a/src/eepp/ui/tools/uisettingspanel.cpp b/src/eepp/ui/tools/uisettingspanel.cpp
new file mode 100644
index 000000000..30d41ce24
--- /dev/null
+++ b/src/eepp/ui/tools/uisettingspanel.cpp
@@ -0,0 +1,976 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define PUGIXML_HEADER_ONLY
+#include
+
+using namespace EE::UI::Models;
+
+namespace EE::UI::Tools {
+
+class UISettingsCategoryModel final : public Model {
+ public:
+ struct Node {
+ std::string id;
+ std::string text;
+ Node* parent{ nullptr };
+ std::vector children;
+ std::vector visibleChildren;
+ };
+
+ static std::shared_ptr
+ create( const std::vector>>& categories,
+ const UnorderedMap& ids ) {
+ return std::make_shared( categories, ids );
+ }
+
+ UISettingsCategoryModel(
+ const std::vector>>& categories,
+ const UnorderedMap& ids ) {
+ mNodes.emplace_back();
+ mRoot = &mNodes.back();
+ for ( const auto& [parent, children] : categories ) {
+ std::string parentId;
+ if ( !children.empty() ) {
+ auto id = ids.find( parent + '/' + children.front() );
+ if ( id != ids.end() ) {
+ auto separator = id->second.find( '.' );
+ parentId = id->second.substr( 0, separator ) + ".*";
+ }
+ }
+ mNodes.push_back( { std::move( parentId ), parent, mRoot } );
+ auto* parentNode = &mNodes.back();
+ mRoot->children.push_back( parentNode );
+ for ( const auto& child : children ) {
+ auto id = ids.find( parent + '/' + child );
+ mNodes.push_back(
+ { id == ids.end() ? std::string{} : id->second, child, parentNode } );
+ parentNode->children.push_back( &mNodes.back() );
+ }
+ }
+ filter( {}, {} );
+ }
+
+ size_t rowCount( const ModelIndex& parent = {} ) const {
+ auto* node = parent.isValid() ? static_cast( parent.internalData() ) : mRoot;
+ return node->visibleChildren.size();
+ }
+
+ size_t columnCount( const ModelIndex& = {} ) const { return 1; }
+
+ ModelIndex index( int row, int column, const ModelIndex& parent = {} ) const {
+ auto* node = parent.isValid() ? static_cast( parent.internalData() ) : mRoot;
+ if ( row < 0 || column != 0 || static_cast( row ) >= node->visibleChildren.size() )
+ return {};
+ return createIndex( row, column, node->visibleChildren[row] );
+ }
+
+ ModelIndex parentIndex( const ModelIndex& index ) const {
+ if ( !index.isValid() )
+ return {};
+ auto* node = static_cast( index.internalData() );
+ if ( !node->parent || node->parent == mRoot )
+ return {};
+ auto* parent = node->parent;
+ auto found =
+ std::find( mRoot->visibleChildren.begin(), mRoot->visibleChildren.end(), parent );
+ return found == mRoot->visibleChildren.end()
+ ? ModelIndex{}
+ : createIndex( std::distance( mRoot->visibleChildren.begin(), found ), 0,
+ parent );
+ }
+
+ Variant data( const ModelIndex& index, ModelRole role = ModelRole::Display ) const {
+ if ( !index.isValid() || role != ModelRole::Display )
+ return {};
+ return Variant( static_cast( index.internalData() )->text );
+ }
+
+ void filter( const std::string_view query,
+ const UnorderedSet& matchingCategories ) {
+ mRoot->visibleChildren.clear();
+ for ( auto* parent : mRoot->children ) {
+ parent->visibleChildren.clear();
+ const bool parentMatches = query.empty() || String::icontains( parent->text, query );
+ for ( auto* child : parent->children ) {
+ if ( parentMatches || String::icontains( child->text, query ) ||
+ matchingCategories.contains( child->id ) )
+ parent->visibleChildren.push_back( child );
+ }
+ if ( !parent->visibleChildren.empty() )
+ mRoot->visibleChildren.push_back( parent );
+ }
+ invalidate( Model::UpdateFlag::InvalidateAllIndexes );
+ }
+
+ private:
+ std::deque mNodes;
+ Node* mRoot{ nullptr };
+};
+
+static constexpr const char* SETTINGS_PANEL_LAYOUT = R"xml(
+
+
+
+
+
+
+
+
+
+
+
+)xml";
+
+static std::string settingsRowLayout( std::string_view control ) {
+ return R"xml(
+
+
+
+
+
+
+
+)xml" + std::string( control ) +
+ R"xml(
+
+
+
+)xml";
+}
+
+class SettingsLayoutTemplate {
+ public:
+ explicit SettingsLayoutTemplate( const std::string& layout ) {
+ [[maybe_unused]] auto result =
+ mDocument.load_string( layout.c_str(), pugi::parse_default | pugi::parse_ws_pcdata );
+ eeASSERT( result );
+ }
+
+ pugi::xml_node root() const { return mDocument.first_child(); }
+
+ private:
+ pugi::xml_document mDocument;
+};
+
+static constexpr const char* SETTINGS_CATEGORY_HEADING_LAYOUT = R"xml(
+
+
+
+
+)xml";
+static constexpr const char* SETTINGS_SUBCATEGORY_HEADING_LAYOUT = R"xml(
+
+)xml";
+static const SettingsLayoutTemplate SETTINGS_BOOL_ROW_LAYOUT( settingsRowLayout(
+ R"xml()xml" ) );
+static const SettingsLayoutTemplate SETTINGS_CHOICE_ROW_LAYOUT( settingsRowLayout(
+ R"xml()xml" ) );
+static const SettingsLayoutTemplate SETTINGS_EDITABLE_CHOICE_ROW_LAYOUT( settingsRowLayout(
+ R"xml()xml" ) );
+static const SettingsLayoutTemplate SETTINGS_INTEGER_ROW_LAYOUT( settingsRowLayout(
+ R"xml()xml" ) );
+static const SettingsLayoutTemplate SETTINGS_TEXT_ROW_LAYOUT( settingsRowLayout(
+ R"xml()xml" ) );
+static const SettingsLayoutTemplate SETTINGS_ACTION_ROW_LAYOUT( settingsRowLayout(
+ R"xml()xml" ) );
+
+static void disableTabFocusTree( Node* node ) {
+ if ( node->isWidget() )
+ node->asType()->unsetTabFocusable();
+ for ( auto* child = node->getFirstChild(); child; child = child->getNextNode() )
+ disableTabFocusTree( child );
+}
+
+struct SettingView {
+ UIWidget* row{ nullptr };
+};
+
+struct SubcategoryHeading {
+ std::string category;
+ String name;
+ UITextView* heading{ nullptr };
+};
+
+struct UISettingsPanel::Impl {
+ EventConnectionList connections;
+ UITextInput* search{ nullptr };
+ UITreeView* categories{ nullptr };
+ UIScrollView* scroll{ nullptr };
+ UILinearLayout* settings{ nullptr };
+ UITextView* pageTitle{ nullptr };
+ std::shared_ptr categoryModel;
+ UIBindingGroup bindingGroup;
+ SettingsModel model;
+ std::vector>> categoryItems;
+ UnorderedMap categoryIds;
+ UnorderedMap categorySearchText;
+ UnorderedMap categoryTitles;
+ UnorderedMap categoryHeadings;
+ UnorderedMap categorySections;
+ UnorderedMap categoryContainers;
+ UnorderedSet materializedCategories;
+ std::vector subcategoryHeadings;
+ std::vector settingViews;
+ std::string selectedCategory;
+ std::string categoryFilter;
+ String searchResultsText{ "Search Results" };
+ bool built{ false };
+};
+
+void SettingsModel::clear() {
+ mCategories.clear();
+ mGroups.clear();
+ mSettings.clear();
+}
+
+bool SettingsModel::addCategory( SettingsCategory category ) {
+ if ( category.id.empty() ||
+ std::any_of( mCategories.begin(), mCategories.end(),
+ [&category]( const auto& item ) { return item.id == category.id; } ) )
+ return false;
+ mCategories.emplace_back( std::move( category ) );
+ return true;
+}
+
+bool SettingsModel::addGroup( SettingsGroup group ) {
+ if ( !hasCategory( group.category ) )
+ return false;
+ mGroups.emplace_back( std::move( group ) );
+ return true;
+}
+
+bool SettingsModel::addSetting( SettingDefinition setting ) {
+ if ( setting.descriptor.id.empty() || !hasCategory( setting.descriptor.category ) ||
+ std::any_of( mSettings.begin(), mSettings.end(), [&setting]( const auto& item ) {
+ return item.descriptor.id == setting.descriptor.id;
+ } ) )
+ return false;
+ mSettings.emplace_back( std::move( setting ) );
+ return true;
+}
+
+bool SettingsModel::hasCategory( const std::string& id ) const {
+ return std::any_of( mCategories.begin(), mCategories.end(),
+ [&id]( const auto& item ) { return item.id == id; } );
+}
+
+UISettingsPanel* UISettingsPanel::New( UIWidget* parent ) {
+ return eeNew( UISettingsPanel, ( parent ) );
+}
+
+UISettingsPanel::UISettingsPanel( UIWidget* parent ) :
+ UILinearLayout( "settingspanel", UIOrientation::Vertical ), mImpl( std::make_unique() ) {
+ setParent( parent );
+ setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent );
+ auto* layout = getUISceneNode()->loadLayoutFromString( SETTINGS_PANEL_LAYOUT, this );
+ mImpl->search = layout->find( "settings_filter" );
+ mImpl->categories = layout->find( "settings_categories" );
+ mImpl->settings = layout->find( "settings_rows" );
+ mImpl->pageTitle = layout->find( "settings_page_title" );
+ mImpl->scroll = layout->find( "settings_scroll" );
+ mImpl->scroll->setVerticalScrollMode( ScrollBarMode::Auto );
+ mImpl->scroll->setHorizontalScrollMode( ScrollBarMode::AlwaysOff );
+ disableTabFocusTree( mImpl->categories->getVerticalScrollBar() );
+ disableTabFocusTree( mImpl->categories->getHorizontalScrollBar() );
+ disableTabFocusTree( mImpl->scroll->getVerticalScrollBar() );
+ disableTabFocusTree( mImpl->scroll->getHorizontalScrollBar() );
+}
+
+UISettingsPanel::~UISettingsPanel() = default;
+
+SettingsModel& UISettingsPanel::getModel() {
+ return mImpl->model;
+}
+
+const SettingsModel& UISettingsPanel::getModel() const {
+ return mImpl->model;
+}
+
+bool UISettingsPanel::addCategory( std::string id, String parent, String name ) {
+ if ( mImpl->built || !mImpl->model.addCategory( { id, parent, name } ) )
+ return false;
+ addCategory( *mImpl, id, parent, name );
+ return true;
+}
+
+bool UISettingsPanel::addGroup( std::string category, String name ) {
+ if ( mImpl->built )
+ return false;
+ return mImpl->model.addGroup(
+ { std::move( category ), std::move( name ), mImpl->model.settings().size() } );
+}
+
+bool UISettingsPanel::addBool( SettingDescriptor descriptor, bool* value,
+ std::function apply ) {
+ return !mImpl->built &&
+ mImpl->model.addSetting(
+ { std::move( descriptor ), BoolPointerSetting{ value, std::move( apply ) } } );
+}
+
+bool UISettingsPanel::addBool( SettingDescriptor descriptor, std::function get,
+ std::function set ) {
+ return !mImpl->built &&
+ mImpl->model.addSetting(
+ { std::move( descriptor ), BoolSetting{ std::move( get ), std::move( set ) } } );
+}
+
+bool UISettingsPanel::addChoice( SettingDescriptor descriptor, std::vector choices,
+ std::function get, std::function set,
+ std::vector choiceDescriptions ) {
+ return !mImpl->built &&
+ mImpl->model.addSetting(
+ { std::move( descriptor ),
+ ChoiceSetting{ std::move( choices ), std::move( choiceDescriptions ),
+ std::move( get ), std::move( set ) } } );
+}
+
+bool UISettingsPanel::addEditableChoice( SettingDescriptor descriptor, std::vector choices,
+ std::function get,
+ std::function set ) {
+ return !mImpl->built &&
+ mImpl->model.addSetting( { std::move( descriptor ),
+ EditableChoiceSetting{ std::move( choices ), std::move( get ),
+ std::move( set ) } } );
+}
+
+bool UISettingsPanel::addInteger( SettingDescriptor descriptor, int min, int max,
+ std::function get, std::function set ) {
+ return !mImpl->built && min <= max &&
+ mImpl->model.addSetting(
+ { std::move( descriptor ),
+ IntegerSetting{ min, max, std::move( get ), std::move( set ) } } );
+}
+
+bool UISettingsPanel::addText( SettingDescriptor descriptor, std::function get,
+ std::function set,
+ bool commitOnFocusLoss ) {
+ return !mImpl->built &&
+ mImpl->model.addSetting(
+ { std::move( descriptor ),
+ TextSetting{ std::move( get ), std::move( set ), commitOnFocusLoss } } );
+}
+
+bool UISettingsPanel::addFloat( SettingDescriptor descriptor, double min, double max, double step,
+ std::function get, std::function set ) {
+ return !mImpl->built && min <= max && step > 0 &&
+ mImpl->model.addSetting(
+ { std::move( descriptor ),
+ FloatSetting{ min, max, step, std::move( get ), std::move( set ) } } );
+}
+
+bool UISettingsPanel::addAction( SettingDescriptor descriptor, String buttonText,
+ std::function action ) {
+ return !mImpl->built && action &&
+ mImpl->model.addSetting(
+ { std::move( descriptor ),
+ ActionSetting{ std::move( buttonText ), std::move( action ) } } );
+}
+
+void UISettingsPanel::build() {
+ if ( mImpl->built )
+ return;
+ mImpl->built = true;
+ mImpl->settings->beginAttributesTransaction();
+ materializeCategory( *mImpl, mImpl->selectedCategory );
+ setupCategories( *mImpl );
+ mImpl->settings->endAttributesTransaction();
+ selectCategory( *mImpl, mImpl->selectedCategory );
+ mImpl->connections += mImpl->search->connect( Event::OnTextChanged, [this]( const Event* ) {
+ String query = mImpl->search->getText();
+ query.trim();
+ const UintPtr debounceTag = reinterpret_cast( this );
+ if ( query.size() < 2 ) {
+ mImpl->search->removeActionsByTag( debounceTag );
+ filter( *mImpl );
+ return;
+ }
+ mImpl->search->debounce( [this] { filter( *mImpl ); }, Milliseconds( 150 ), debounceTag );
+ } );
+}
+
+void UISettingsPanel::selectCategory( const std::string& category ) {
+ selectCategory( *mImpl, category );
+}
+
+void UISettingsPanel::setCategoryEnabled( const std::string& category, bool enabled,
+ const std::string& excludedSetting ) {
+ setCategoryEnabled( *mImpl, category, enabled, excludedSetting );
+}
+
+void UISettingsPanel::refreshTextSetting( const std::string& id ) {
+ refreshTextSetting( *mImpl, id );
+}
+
+void UISettingsPanel::setSearchResultsText( String text ) {
+ mImpl->searchResultsText = std::move( text );
+}
+
+void UISettingsPanel::setFilter( String filterText ) {
+ const UintPtr debounceTag = reinterpret_cast( this );
+ mImpl->search->setText( std::move( filterText ) );
+ mImpl->search->removeActionsByTag( debounceTag );
+ filter( *mImpl );
+}
+
+void UISettingsPanel::focusSearch() {
+ mImpl->search->setFocus();
+ mImpl->search->getDocument().selectAll();
+}
+
+void UISettingsPanel::focusCategories() {
+ auto selected = mImpl->categories->getSelection().first();
+ if ( selected.isValid() )
+ mImpl->categories->setSelection( selected );
+ mImpl->categories->setFocus();
+}
+
+bool UISettingsPanel::isBuilt() const {
+ return mImpl->built;
+}
+
+void UISettingsPanel::addCategory( Impl& panel, const std::string& id, const String& parent,
+ const String& name ) {
+ const auto parentText = parent.toUtf8();
+ const auto nameText = name.toUtf8();
+ auto parentItems =
+ std::find_if( panel.categoryItems.begin(), panel.categoryItems.end(),
+ [&parentText]( const auto& item ) { return item.first == parentText; } );
+ if ( parentItems == panel.categoryItems.end() ) {
+ panel.categoryItems.emplace_back( parentText, std::vector{ nameText } );
+ } else {
+ parentItems->second.emplace_back( nameText );
+ }
+ panel.categoryIds[parentText + '/' + nameText] = id;
+ panel.categorySearchText[id] = parent + " " + name;
+ panel.categoryTitles[id] = name;
+ auto* section =
+ getUISceneNode()->loadLayoutFromString( SETTINGS_CATEGORY_HEADING_LAYOUT, panel.settings );
+ auto* heading = section->find( "settings_category_heading" );
+ heading->setText( name );
+ heading->setId( "settings_category_" + id );
+ panel.categoryHeadings[id] = heading;
+ panel.categorySections[id] = section;
+ panel.categoryContainers[id] = section->find( "settings_category_rows" );
+ if ( panel.selectedCategory.empty() )
+ panel.selectedCategory = id;
+}
+
+void UISettingsPanel::selectCategory( Impl& panel, const std::string& category ) {
+ if ( category.empty() || !panel.categories )
+ return;
+ String title;
+ if ( String::endsWith( category, ".*" ) ) {
+ const std::string prefix = category.substr( 0, category.size() - 1 );
+ for ( const auto& [parent, children] : panel.categoryItems ) {
+ if ( children.empty() )
+ continue;
+ auto id = panel.categoryIds.find( parent + '/' + children.front() );
+ if ( id != panel.categoryIds.end() && String::startsWith( id->second, prefix ) ) {
+ title = String::fromUtf8( parent );
+ break;
+ }
+ }
+ } else if ( auto found = panel.categoryTitles.find( category );
+ found != panel.categoryTitles.end() ) {
+ title = found->second;
+ }
+ if ( title.empty() )
+ return;
+ panel.selectedCategory = category;
+ auto index = panel.categories->findRowWithText(
+ title.toUtf8(), true, UIAbstractView::FindRowWithTextMatchKind::Equals );
+ if ( index.isValid() )
+ panel.categories->setSelection( index );
+ filter( panel );
+ panel.scroll->getVerticalScrollBar()->setValue( 0, false );
+}
+
+void UISettingsPanel::addSubcategoryHeading( Impl& panel, const std::string& category,
+ const String& name ) {
+ panel.model.addGroup( { category, name, panel.model.settings().size() } );
+}
+
+void UISettingsPanel::setupCategories( Impl& panel ) {
+ auto model = UISettingsCategoryModel::create( panel.categoryItems, panel.categoryIds );
+ panel.categoryModel = model;
+ panel.categories->setHeadersVisible( false );
+ panel.categories->setAutoExpandOnSingleColumn( true );
+ panel.categories->setFocusOnSelection( true );
+ panel.categories->setModel( model );
+ panel.categories->expandAll();
+ panel.connections +=
+ panel.categories->connect( Event::OnSelectionChanged, [this, &panel]( const Event* ) {
+ auto index = panel.categories->getSelection().first();
+ if ( !index.isValid() )
+ return;
+ auto* node = static_cast( index.internalData() );
+ if ( !node || node->id.empty() )
+ return;
+ panel.selectedCategory = node->id;
+ panel.pageTitle->setText( node->text );
+ filter( panel );
+ panel.scroll->getVerticalScrollBar()->setValue( 0, false );
+ } );
+}
+
+UIWidget* UISettingsPanel::createRow( Impl& panel, SettingDefinition& setting, SettingView& view,
+ pugi::xml_node layout ) {
+ auto& binding = setting.descriptor;
+ auto container = panel.categoryContainers.find( binding.category );
+ eeASSERT( container != panel.categoryContainers.end() );
+ auto* row = getUISceneNode()->loadLayoutNodes( layout, container->second, 0 );
+ row->setId( "setting_" + binding.id );
+ row->find( "setting_name" )->setText( binding.name );
+ auto* description = row->find( "setting_description" );
+ description->setText( binding.description );
+ view.row = row;
+ return row;
+}
+
+UICheckBox* UISettingsPanel::createBoolControl( Impl& panel, SettingDefinition& setting,
+ SettingView& view ) {
+ auto* row = createRow( panel, setting, view, SETTINGS_BOOL_ROW_LAYOUT.root() );
+ row->addClass( "settings_boolean_option" );
+ auto* check = row->find( "setting_control_widget" );
+ auto toggle = [check]( const Event* event ) {
+ if ( event->asMouseEvent()->getFlags() & EE_BUTTON_LMASK )
+ check->setChecked( !check->isChecked() );
+ };
+ panel.connections +=
+ row->find( "setting_name" )->connect( Event::MouseClick, toggle );
+ panel.connections +=
+ row->find( "setting_description" )->connect( Event::MouseClick, toggle );
+ panel.connections += row->find( "setting_info" )
+ ->connect( Event::MouseClick, std::move( toggle ) );
+ return check;
+}
+
+void UISettingsPanel::refreshTextSetting( Impl& panel, const std::string& id ) {
+ const auto& settings = panel.model.settings();
+ for ( size_t i = 0; i < settings.size(); ++i ) {
+ if ( settings[i].descriptor.id != id || i >= panel.settingViews.size() ||
+ !panel.settingViews[i].row )
+ continue;
+ auto* value = std::get_if( &settings[i].value );
+ auto* input = panel.settingViews[i].row->find( "setting_control_widget" );
+ if ( value && input )
+ input->setText( String::fromUtf8( value->get() ) );
+ return;
+ }
+}
+
+static void setNodeTreeEnabled( Node* node, bool enabled );
+
+void UISettingsPanel::materializeCategory( Impl& panel, const std::string& category ) {
+ if ( category.empty() || panel.materializedCategories.contains( category ) )
+ return;
+ auto container = panel.categoryContainers.find( category );
+ if ( container == panel.categoryContainers.end() )
+ return;
+ auto& settings = panel.model.settings();
+ panel.settingViews.resize( settings.size() );
+ container->second->beginAttributesTransaction();
+ for ( size_t i = 0; i < settings.size(); ++i ) {
+ auto& setting = settings[i];
+ if ( setting.descriptor.category != category )
+ continue;
+ for ( const auto& group : panel.model.groups() ) {
+ if ( group.category != category || group.beforeSetting != i )
+ continue;
+ auto* heading =
+ getUISceneNode()
+ ->loadLayoutFromString( SETTINGS_SUBCATEGORY_HEADING_LAYOUT, container->second )
+ ->asType();
+ heading->setText( group.name );
+ panel.subcategoryHeadings.push_back( { group.category, group.name, heading } );
+ }
+ auto& view = panel.settingViews[i];
+ if ( auto* value = std::get_if( &setting.value ) ) {
+ auto* check = createBoolControl( panel, setting, view );
+ auto binding = UIDataBind::New( value->value, check,
+ UIValueConverter::converterBool() );
+ binding->onValueChangeCb = value->apply;
+ panel.bindingGroup += std::move( binding );
+ } else if ( auto* value = std::get_if( &setting.value ) ) {
+ auto* check = createBoolControl( panel, setting, view );
+ check->setChecked( value->get() );
+ panel.connections +=
+ check->connect( Event::OnValueChange, [check, value]( const Event* ) {
+ value->set( check->isChecked() );
+ } );
+ } else if ( auto* value = std::get_if( &setting.value ) ) {
+ auto* row = createRow( panel, setting, view, SETTINGS_CHOICE_ROW_LAYOUT.root() );
+ auto* dropDown = row->find( "setting_control_widget" );
+ auto model = ItemListOwnerModel::create( value->choices );
+ dropDown->setModel( model );
+ const size_t selected = value->get();
+ if ( selected < value->choices.size() ) {
+ dropDown->getListView()->getSelection().set( model->index( selected, 0 ) );
+ dropDown->setText( value->choices[selected] );
+ }
+ if ( selected < value->descriptions.size() )
+ dropDown->setTooltipText( value->descriptions[selected] );
+ panel.connections +=
+ dropDown->connect( Event::OnValueChange, [dropDown, value]( const Event* ) {
+ if ( dropDown->getListView()->getSelection().isEmpty() )
+ return;
+ const size_t selected = dropDown->getListView()->getSelection().first().row();
+ if ( selected < value->descriptions.size() )
+ dropDown->setTooltipText( value->descriptions[selected] );
+ value->set( selected );
+ } );
+ } else if ( auto* value = std::get_if( &setting.value ) ) {
+ auto* row =
+ createRow( panel, setting, view, SETTINGS_EDITABLE_CHOICE_ROW_LAYOUT.root() );
+ auto* combo = row->find( "setting_control_widget" );
+ for ( const auto& choice : value->choices )
+ combo->getListBox()->addListBoxItem( choice );
+ combo->setText( value->get() );
+ panel.connections +=
+ combo->connect( Event::OnValueChange, [combo, value]( const Event* ) {
+ if ( !value->set( combo->getText() ) ) {
+ combo->addClass( "error" );
+ combo->getDropDownList()->addClass( "error" );
+ return;
+ }
+ combo->removeClass( "error" );
+ combo->getDropDownList()->removeClass( "error" );
+ } );
+ } else if ( auto* value = std::get_if( &setting.value ) ) {
+ auto* row = createRow( panel, setting, view, SETTINGS_INTEGER_ROW_LAYOUT.root() );
+ auto* spin = row->find( "setting_control_widget" );
+ spin->setMinValue( value->min )->setMaxValue( value->max );
+ spin->unsetTabFocusable();
+ spin->getButtonPushUp()->asType()->unsetTabFocusable();
+ spin->getButtonPushDown()->asType()->unsetTabFocusable();
+ spin->setValue( value->get() );
+ panel.connections +=
+ spin->connect( Event::OnValueChange, [spin, value]( const Event* ) {
+ value->set( static_cast( spin->getValue() ) );
+ } );
+ } else if ( auto* value = std::get_if( &setting.value ) ) {
+ auto* row = createRow( panel, setting, view, SETTINGS_TEXT_ROW_LAYOUT.root() );
+ auto* input = row->find( "setting_control_widget" );
+ if ( value->password )
+ input->setMode( UITextInput::TextInputMode::Password );
+ input->setText( String::fromUtf8( value->get() ) );
+ auto commit = [input, value] {
+ if ( !value->set( input->getText().toUtf8() ) ) {
+ input->addClass( "error" );
+ return;
+ }
+ input->removeClass( "error" );
+ };
+ if ( value->commitOnFocusLoss ) {
+ const auto debounceTag = reinterpret_cast( input );
+ panel.connections += input->connect(
+ Event::OnTextChanged, [input, commit, debounceTag]( const Event* ) {
+ input->debounce( commit, Milliseconds( 500 ), debounceTag );
+ } );
+ auto flush = [input, commit, debounceTag]( const Event* ) {
+ input->removeActionsByTag( debounceTag );
+ commit();
+ };
+ panel.connections += input->connect( Event::OnPressEnter, flush );
+ panel.connections += input->connect( Event::OnFocusLoss, std::move( flush ) );
+ } else {
+ panel.connections +=
+ input->connect( Event::OnTextChanged,
+ [commit = std::move( commit )]( const Event* ) { commit(); } );
+ }
+ } else if ( auto* value = std::get_if( &setting.value ) ) {
+ auto* row = createRow( panel, setting, view, SETTINGS_INTEGER_ROW_LAYOUT.root() );
+ auto* spin = row->find( "setting_control_widget" );
+ spin->setMinValue( value->min )->setMaxValue( value->max )->setClickStep( value->step );
+ spin->allowFloatingPoint( true )->setValue( value->get() );
+ spin->unsetTabFocusable();
+ spin->getButtonPushUp()->asType()->unsetTabFocusable();
+ spin->getButtonPushDown()->asType()->unsetTabFocusable();
+ panel.connections +=
+ spin->connect( Event::OnValueChange,
+ [spin, value]( const Event* ) { value->set( spin->getValue() ); } );
+ } else if ( auto* value = std::get_if( &setting.value ) ) {
+ auto* row = createRow( panel, setting, view, SETTINGS_ACTION_ROW_LAYOUT.root() );
+ auto* button = row->find( "setting_control_widget" );
+ button->setText( value->buttonText );
+ panel.connections += button->connect( Event::MouseClick, [value]( const Event* event ) {
+ if ( event->asMouseEvent()->getFlags() & EE_BUTTON_LMASK )
+ value->action();
+ } );
+ }
+ if ( view.row && !setting.enabled )
+ setNodeTreeEnabled( view.row, false );
+ }
+ container->second->endAttributesTransaction();
+ panel.materializedCategories.insert( category );
+}
+
+void UISettingsPanel::materializeVisibleSettings( Impl& panel, const String& query ) {
+ const std::string queryUtf8 = query.toUtf8();
+ const bool aggregate = String::endsWith( panel.selectedCategory, ".*" );
+ const std::string aggregatePrefix =
+ aggregate ? panel.selectedCategory.substr( 0, panel.selectedCategory.size() - 1 )
+ : std::string{};
+ for ( const auto& category : panel.model.categories() ) {
+ bool materialize =
+ query.empty() ? category.id == panel.selectedCategory ||
+ ( aggregate && String::startsWith( category.id, aggregatePrefix ) )
+ : String::icontains( category.parent, query ) ||
+ String::icontains( category.name, query );
+ if ( !materialize && !query.empty() ) {
+ for ( const auto& setting : panel.model.settings() ) {
+ const auto& descriptor = setting.descriptor;
+ if ( descriptor.category == category.id &&
+ ( String::icontains( descriptor.name, query ) ||
+ String::icontains( descriptor.description, query ) ||
+ String::icontains( descriptor.group, query ) ||
+ String::icontains( descriptor.id, queryUtf8 ) ) ) {
+ materialize = true;
+ break;
+ }
+ }
+ }
+ if ( materialize )
+ materializeCategory( panel, category.id );
+ }
+}
+
+static void setNodeTreeEnabled( Node* node, bool enabled ) {
+ node->setEnabled( enabled );
+ for ( Node* child = node->getFirstChild(); child; child = child->getNextNode() )
+ setNodeTreeEnabled( child, enabled );
+}
+
+void UISettingsPanel::setCategoryEnabled( Impl& panel, const std::string& category, bool enabled,
+ const std::string& excludedSetting ) {
+ auto& settings = panel.model.settings();
+ for ( size_t i = 0; i < settings.size(); ++i ) {
+ auto& setting = settings[i];
+ const auto& descriptor = setting.descriptor;
+ if ( descriptor.category != category || descriptor.id == excludedSetting )
+ continue;
+ setting.enabled = enabled;
+ if ( i < panel.settingViews.size() && panel.settingViews[i].row )
+ setNodeTreeEnabled( panel.settingViews[i].row, enabled );
+ }
+}
+
+void UISettingsPanel::filter( Impl& panel ) {
+ String query = panel.search ? panel.search->getText() : String{};
+ query.trim().toLower();
+ if ( query.size() < 2 )
+ query.clear();
+ materializeVisibleSettings( panel, query );
+ if ( !query.empty() ) {
+ panel.pageTitle->setText( panel.searchResultsText );
+ } else if ( auto title = panel.categoryTitles.find( panel.selectedCategory );
+ title != panel.categoryTitles.end() ) {
+ panel.pageTitle->setText( title->second );
+ }
+ UnorderedSet matchingCategories;
+ const std::string queryUtf8 = query.toUtf8();
+ const bool aggregate = String::endsWith( panel.selectedCategory, ".*" );
+ const std::string aggregatePrefix =
+ aggregate ? panel.selectedCategory.substr( 0, panel.selectedCategory.size() - 1 )
+ : std::string{};
+ if ( !query.empty() ) {
+ for ( const auto& setting : panel.model.settings() ) {
+ const auto& binding = setting.descriptor;
+ if ( String::icontains( binding.name, query ) ||
+ String::icontains( binding.description, query ) ||
+ String::icontains( binding.group, query ) ||
+ String::icontains( binding.id, queryUtf8 ) )
+ matchingCategories.insert( binding.category );
+ }
+ }
+ if ( queryUtf8 != panel.categoryFilter ) {
+ panel.categoryFilter = queryUtf8;
+ if ( auto model = std::static_pointer_cast( panel.categoryModel ) )
+ model->filter( queryUtf8, matchingCategories );
+ }
+ panel.categories->expandAll();
+ for ( auto& [category, heading] : panel.categoryHeadings )
+ heading->setVisible( query.empty() && aggregate &&
+ String::startsWith( category, aggregatePrefix ) );
+ const auto& settings = panel.model.settings();
+ for ( size_t i = 0; i < settings.size(); ++i ) {
+ const auto& binding = settings[i].descriptor;
+ const auto categoryName = panel.categorySearchText.find( binding.category );
+ const bool categoryMatches = categoryName != panel.categorySearchText.end() &&
+ String::icontains( categoryName->second, query );
+ const bool matches =
+ query.empty()
+ ? binding.category == panel.selectedCategory ||
+ ( aggregate && String::startsWith( binding.category, aggregatePrefix ) )
+ : categoryMatches || String::icontains( binding.name, query ) ||
+ String::icontains( binding.description, query ) ||
+ String::icontains( binding.group, query ) ||
+ String::icontains( binding.id, queryUtf8 );
+ if ( i < panel.settingViews.size() && panel.settingViews[i].row )
+ panel.settingViews[i].row->setVisible( matches );
+ }
+ for ( const auto& category : panel.model.categories() ) {
+ auto section = panel.categorySections.find( category.id );
+ if ( section == panel.categorySections.end() )
+ continue;
+ bool visible = false;
+ for ( size_t i = 0; i < settings.size(); ++i ) {
+ if ( settings[i].descriptor.category == category.id && i < panel.settingViews.size() &&
+ panel.settingViews[i].row && panel.settingViews[i].row->isVisible() ) {
+ visible = true;
+ break;
+ }
+ }
+ section->second->setVisible( visible );
+ }
+ for ( auto& subcategory : panel.subcategoryHeadings ) {
+ if ( !subcategory.heading )
+ continue;
+ const bool hasVisibleSetting = std::any_of(
+ settings.begin(), settings.end(), [&panel, &subcategory]( const auto& setting ) {
+ const auto& binding = setting.descriptor;
+ const size_t index = &setting - panel.model.settings().data();
+ return binding.category == subcategory.category &&
+ binding.group == subcategory.name && index < panel.settingViews.size() &&
+ panel.settingViews[index].row && panel.settingViews[index].row->isVisible();
+ } );
+ subcategory.heading->setVisible( hasVisibleSetting );
+ }
+}
+
+} // namespace EE::UI::Tools
diff --git a/src/tests/unit_tests/uisettingspanel_tests.cpp b/src/tests/unit_tests/uisettingspanel_tests.cpp
new file mode 100644
index 000000000..a4db1c0ee
--- /dev/null
+++ b/src/tests/unit_tests/uisettingspanel_tests.cpp
@@ -0,0 +1,135 @@
+#include "utest.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace EE;
+using namespace EE::System;
+using namespace EE::UI;
+using namespace EE::UI::Tools;
+using namespace EE::Window;
+
+UTEST( UISettingsPanelModel, validatesCategoriesGroupsAndSettings ) {
+ SettingsModel model;
+
+ EXPECT_FALSE( model.addCategory( { {}, "General", "Behavior" } ) );
+ EXPECT_TRUE( model.addCategory( { "general.behavior", "General", "Behavior" } ) );
+ EXPECT_FALSE( model.addCategory( { "general.behavior", "General", "Other" } ) );
+ EXPECT_TRUE( model.hasCategory( "general.behavior" ) );
+ EXPECT_FALSE( model.hasCategory( "general.missing" ) );
+
+ EXPECT_FALSE( model.addGroup( { "general.missing", "Display", 0 } ) );
+ EXPECT_TRUE( model.addGroup( { "general.behavior", "Display", 0 } ) );
+
+ bool value = false;
+ EXPECT_FALSE( model.addSetting( { { {}, "general.behavior", "Empty", "Empty identifier", {} },
+ BoolPointerSetting{ &value, {} } } ) );
+ EXPECT_FALSE(
+ model.addSetting( { { "missing", "general.missing", "Missing", "Missing category", {} },
+ BoolPointerSetting{ &value, {} } } ) );
+ EXPECT_TRUE( model.addSetting(
+ { { "enabled", "general.behavior", "Enabled", "Enable the option", "Display" },
+ BoolPointerSetting{ &value, {} } } ) );
+ EXPECT_FALSE( model.addSetting(
+ { { "enabled", "general.behavior", "Duplicate", "Duplicate identifier", {} },
+ BoolPointerSetting{ &value, {} } } ) );
+
+ EXPECT_EQ( 1u, model.categories().size() );
+ EXPECT_EQ( 1u, model.groups().size() );
+ EXPECT_EQ( 1u, model.settings().size() );
+ EXPECT_STDSTREQ( "enabled", model.settings().front().descriptor.id );
+}
+
+UTEST( UISettingsPanelModel, clearRemovesAllRules ) {
+ SettingsModel model;
+ bool value = true;
+ EXPECT_TRUE( model.addCategory( { "editor.document", "Editor", "Document" } ) );
+ EXPECT_TRUE( model.addGroup( { "editor.document", "Files", 0 } ) );
+ EXPECT_TRUE(
+ model.addSetting( { { "trim", "editor.document", "Trim", "Trim whitespace", "Files" },
+ BoolPointerSetting{ &value, {} } } ) );
+
+ model.clear();
+
+ EXPECT_TRUE( model.categories().empty() );
+ EXPECT_TRUE( model.groups().empty() );
+ EXPECT_TRUE( model.settings().empty() );
+}
+
+UTEST( UISettingsPanel, buildsAndMaterializesCategoriesLazily ) {
+ UIApplication app(
+ WindowSettings( 800, 600, "eepp - UISettingsPanel Test", WindowStyle::Default,
+ WindowBackend::Default, 32 ),
+ UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
+ auto* panel = UISettingsPanel::New( app.getUI()->getRoot() );
+ bool firstValue = false;
+ bool secondValue = true;
+
+ EXPECT_TRUE( panel->addCategory( "general.behavior", "General", "Behavior" ) );
+ EXPECT_TRUE( panel->addCategory( "editor.display", "Editor", "Display" ) );
+ EXPECT_TRUE( panel->addBool( { "first", "general.behavior", "First", "The first setting", {} },
+ &firstValue ) );
+ EXPECT_TRUE( panel->addBool( { "second", "editor.display", "Second", "The second setting", {} },
+ &secondValue ) );
+ EXPECT_EQ( nullptr, panel->find( "setting_first" ) );
+ EXPECT_EQ( nullptr, panel->find( "setting_second" ) );
+
+ panel->build();
+
+ EXPECT_TRUE( panel->isBuilt() );
+ auto* firstRow = panel->find( "setting_first" );
+ EXPECT_NE( nullptr, firstRow );
+ EXPECT_TRUE( firstRow->isVisible() );
+ EXPECT_EQ( nullptr, panel->find( "setting_second" ) );
+ EXPECT_FALSE( panel->addCategory( "late.category", "Late", "Category" ) );
+
+ panel->selectCategory( "editor.display" );
+ auto* secondRow = panel->find( "setting_second" );
+ EXPECT_NE( nullptr, secondRow );
+ EXPECT_FALSE( firstRow->isVisible() );
+ EXPECT_TRUE( secondRow->isVisible() );
+ EXPECT_STDSTREQ( "Display",
+ panel->find( "settings_page_title" )->getText().toUtf8() );
+
+ panel->setCategoryEnabled( "editor.display", false );
+ EXPECT_FALSE( secondRow->isEnabled() );
+ panel->setCategoryEnabled( "editor.display", true );
+ EXPECT_TRUE( secondRow->isEnabled() );
+}
+
+UTEST( UISettingsPanel, filtersAcrossUnmaterializedCategories ) {
+ UIApplication app(
+ WindowSettings( 800, 600, "eepp - UISettingsPanel Filter Test", WindowStyle::Default,
+ WindowBackend::Default, 32 ),
+ UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
+ auto* panel = UISettingsPanel::New( app.getUI()->getRoot() );
+ bool firstValue = false;
+ bool secondValue = false;
+ panel->setSearchResultsText( "Filtered Settings" );
+ EXPECT_TRUE( panel->addCategory( "general.behavior", "General", "Behavior" ) );
+ EXPECT_TRUE( panel->addCategory( "editor.display", "Editor", "Display" ) );
+ EXPECT_TRUE( panel->addBool( { "first", "general.behavior", "First", "Ordinary option", {} },
+ &firstValue ) );
+ EXPECT_TRUE( panel->addBool(
+ { "second", "editor.display", "Second", "Unique searchable phrase", {} }, &secondValue ) );
+ panel->build();
+
+ panel->setFilter( "searchable" );
+
+ auto* firstRow = panel->find( "setting_first" );
+ auto* secondRow = panel->find( "setting_second" );
+ EXPECT_NE( nullptr, secondRow );
+ EXPECT_FALSE( firstRow->isVisible() );
+ EXPECT_TRUE( secondRow->isVisible() );
+ EXPECT_STDSTREQ( "Filtered Settings",
+ panel->find( "settings_page_title" )->getText().toUtf8() );
+
+ panel->setFilter( {} );
+ EXPECT_TRUE( firstRow->isVisible() );
+ EXPECT_FALSE( secondRow->isVisible() );
+}
diff --git a/src/tools/ecode/plugins/git/gitplugin.cpp b/src/tools/ecode/plugins/git/gitplugin.cpp
index 618d7a260..6bfb22c98 100644
--- a/src/tools/ecode/plugins/git/gitplugin.cpp
+++ b/src/tools/ecode/plugins/git/gitplugin.cpp
@@ -31,6 +31,7 @@
#include
#include
#include
+#include
#include
#include
diff --git a/src/tools/ecode/settingsmodel.hpp b/src/tools/ecode/settingsmodel.hpp
deleted file mode 100644
index 2a2354146..000000000
--- a/src/tools/ecode/settingsmodel.hpp
+++ /dev/null
@@ -1,148 +0,0 @@
-#ifndef ECODE_SETTINGSMODEL_HPP
-#define ECODE_SETTINGSMODEL_HPP
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-namespace ecode {
-
-struct SettingDescriptor {
- std::string id;
- std::string category;
- String name;
- String description;
- String group;
-};
-
-struct BoolPointerSetting {
- bool* value{ nullptr };
- std::function apply;
-};
-
-struct BoolSetting {
- std::function get;
- std::function set;
-};
-
-struct ChoiceSetting {
- std::vector choices;
- std::vector descriptions;
- std::function get;
- std::function set;
-};
-
-struct EditableChoiceSetting {
- std::vector choices;
- std::function get;
- std::function set;
-};
-
-struct IntegerSetting {
- int min{ 0 };
- int max{ 0 };
- std::function get;
- std::function set;
-};
-
-struct TextSetting {
- std::function get;
- std::function set;
- bool commitOnFocusLoss{ false };
- bool password{ false };
-};
-
-struct FloatSetting {
- double min{ 0 };
- double max{ 0 };
- double step{ 0 };
- std::function get;
- std::function set;
-};
-
-struct ActionSetting {
- String buttonText;
- std::function action;
-};
-
-using SettingValue =
- std::variant;
-
-struct SettingDefinition {
- SettingDescriptor descriptor;
- SettingValue value;
- bool enabled{ true };
-};
-
-struct SettingsCategory {
- std::string id;
- String parent;
- String name;
-};
-
-struct SettingsGroup {
- std::string category;
- String name;
- size_t beforeSetting{ 0 };
-};
-
-class SettingsModel {
- public:
- void clear() {
- mCategories.clear();
- mGroups.clear();
- mSettings.clear();
- }
-
- bool addCategory( SettingsCategory category ) {
- if ( category.id.empty() ||
- std::any_of( mCategories.begin(), mCategories.end(),
- [&category]( const auto& item ) { return item.id == category.id; } ) )
- return false;
- mCategories.emplace_back( std::move( category ) );
- return true;
- }
-
- bool addGroup( SettingsGroup group ) {
- if ( !hasCategory( group.category ) )
- return false;
- mGroups.emplace_back( std::move( group ) );
- return true;
- }
-
- bool addSetting( SettingDefinition setting ) {
- if ( setting.descriptor.id.empty() || !hasCategory( setting.descriptor.category ) ||
- std::any_of( mSettings.begin(), mSettings.end(), [&setting]( const auto& item ) {
- return item.descriptor.id == setting.descriptor.id;
- } ) )
- return false;
- mSettings.emplace_back( std::move( setting ) );
- return true;
- }
-
- bool hasCategory( const std::string& id ) const {
- return std::any_of( mCategories.begin(), mCategories.end(),
- [&id]( const auto& item ) { return item.id == id; } );
- }
-
- const std::vector& categories() const { return mCategories; }
-
- const std::vector& groups() const { return mGroups; }
-
- std::vector& settings() { return mSettings; }
-
- const std::vector& settings() const { return mSettings; }
-
- private:
- std::vector mCategories;
- std::vector mGroups;
- std::vector mSettings;
-};
-
-} // namespace ecode
-
-#endif
diff --git a/src/tools/ecode/settingspage.hpp b/src/tools/ecode/settingspage.hpp
index b12d96f6b..634a927ee 100644
--- a/src/tools/ecode/settingspage.hpp
+++ b/src/tools/ecode/settingspage.hpp
@@ -2,7 +2,12 @@
#define ECODE_SETTINGSPAGE_HPP
#include "settingsdocument.hpp"
-#include "settingsmodel.hpp"
+
+#include
+
+using namespace EE;
+using namespace EE::System;
+using namespace EE::UI::Tools;
namespace ecode {
diff --git a/src/tools/ecode/settingspanel.cpp b/src/tools/ecode/settingspanel.cpp
index d1d910da0..412f68724 100644
--- a/src/tools/ecode/settingspanel.cpp
+++ b/src/tools/ecode/settingspanel.cpp
@@ -6,308 +6,10 @@
#include "settingsdocument.hpp"
#include "settingspage.hpp"
#include "uitreeviewfs.hpp"
-#include
#include
-#define PUGIXML_HEADER_ONLY
-#include
-
-using namespace EE::UI::Models;
namespace ecode {
-class SettingsCategoryModel final : public Model {
- public:
- struct Node {
- std::string id;
- std::string text;
- Node* parent{ nullptr };
- std::vector children;
- std::vector visibleChildren;
- };
-
- static std::shared_ptr
- create( const std::vector>>& categories,
- const UnorderedMap& ids ) {
- return std::make_shared( categories, ids );
- }
-
- SettingsCategoryModel(
- const std::vector>>& categories,
- const UnorderedMap& ids ) {
- mNodes.emplace_back();
- mRoot = &mNodes.back();
- for ( const auto& [parent, children] : categories ) {
- std::string parentId;
- if ( !children.empty() ) {
- auto id = ids.find( parent + '/' + children.front() );
- if ( id != ids.end() ) {
- auto separator = id->second.find( '.' );
- parentId = id->second.substr( 0, separator ) + ".*";
- }
- }
- mNodes.push_back( { std::move( parentId ), parent, mRoot } );
- auto* parentNode = &mNodes.back();
- mRoot->children.push_back( parentNode );
- for ( const auto& child : children ) {
- auto id = ids.find( parent + '/' + child );
- mNodes.push_back(
- { id == ids.end() ? std::string{} : id->second, child, parentNode } );
- parentNode->children.push_back( &mNodes.back() );
- }
- }
- filter( {}, {} );
- }
-
- size_t rowCount( const ModelIndex& parent = {} ) const {
- auto* node = parent.isValid() ? static_cast( parent.internalData() ) : mRoot;
- return node->visibleChildren.size();
- }
-
- size_t columnCount( const ModelIndex& = {} ) const { return 1; }
-
- ModelIndex index( int row, int column, const ModelIndex& parent = {} ) const {
- auto* node = parent.isValid() ? static_cast( parent.internalData() ) : mRoot;
- if ( row < 0 || column != 0 || static_cast( row ) >= node->visibleChildren.size() )
- return {};
- return createIndex( row, column, node->visibleChildren[row] );
- }
-
- ModelIndex parentIndex( const ModelIndex& index ) const {
- if ( !index.isValid() )
- return {};
- auto* node = static_cast( index.internalData() );
- if ( !node->parent || node->parent == mRoot )
- return {};
- auto* parent = node->parent;
- auto found =
- std::find( mRoot->visibleChildren.begin(), mRoot->visibleChildren.end(), parent );
- return found == mRoot->visibleChildren.end()
- ? ModelIndex{}
- : createIndex( std::distance( mRoot->visibleChildren.begin(), found ), 0,
- parent );
- }
-
- Variant data( const ModelIndex& index, ModelRole role = ModelRole::Display ) const {
- if ( !index.isValid() || role != ModelRole::Display )
- return {};
- return Variant( static_cast( index.internalData() )->text );
- }
-
- void filter( const std::string_view query,
- const UnorderedSet& matchingCategories ) {
- mRoot->visibleChildren.clear();
- for ( auto* parent : mRoot->children ) {
- parent->visibleChildren.clear();
- const bool parentMatches = query.empty() || String::icontains( parent->text, query );
- for ( auto* child : parent->children ) {
- if ( parentMatches || String::icontains( child->text, query ) ||
- matchingCategories.contains( child->id ) )
- parent->visibleChildren.push_back( child );
- }
- if ( !parent->visibleChildren.empty() )
- mRoot->visibleChildren.push_back( parent );
- }
- invalidate( Model::UpdateFlag::InvalidateAllIndexes );
- }
-
- private:
- std::deque mNodes;
- Node* mRoot{ nullptr };
-};
-
-static constexpr const char* SETTINGS_PANEL_LAYOUT = R"xml(
-
-
-
-
-
-
-
-
-
-
-
-)xml";
-
-static std::string settingsRowLayout( std::string_view control ) {
- return R"xml(
-
-
-
-
-
-
-
-)xml" + std::string( control ) +
- R"xml(
-
-
-
-)xml";
-}
-
-class SettingsLayoutTemplate {
- public:
- explicit SettingsLayoutTemplate( const std::string& layout ) {
- [[maybe_unused]] auto result =
- mDocument.load_string( layout.c_str(), pugi::parse_default | pugi::parse_ws_pcdata );
- eeASSERT( result );
- }
-
- pugi::xml_node root() const { return mDocument.first_child(); }
-
- private:
- pugi::xml_document mDocument;
-};
-
-static constexpr const char* SETTINGS_CATEGORY_HEADING_LAYOUT = R"xml(
-
-
-
-
-)xml";
-static constexpr const char* SETTINGS_SUBCATEGORY_HEADING_LAYOUT = R"xml(
-
-)xml";
-static const SettingsLayoutTemplate SETTINGS_BOOL_ROW_LAYOUT( settingsRowLayout(
- R"xml()xml" ) );
-static const SettingsLayoutTemplate SETTINGS_CHOICE_ROW_LAYOUT( settingsRowLayout(
- R"xml()xml" ) );
-static const SettingsLayoutTemplate SETTINGS_EDITABLE_CHOICE_ROW_LAYOUT( settingsRowLayout(
- R"xml()xml" ) );
-static const SettingsLayoutTemplate SETTINGS_INTEGER_ROW_LAYOUT( settingsRowLayout(
- R"xml()xml" ) );
-static const SettingsLayoutTemplate SETTINGS_TEXT_ROW_LAYOUT( settingsRowLayout(
- R"xml()xml" ) );
-static const SettingsLayoutTemplate SETTINGS_ACTION_ROW_LAYOUT( settingsRowLayout(
- R"xml()xml" ) );
-
-static void disableTabFocusTree( Node* node ) {
- if ( node->isWidget() )
- node->asType()->unsetTabFocusable();
- for ( auto* child = node->getFirstChild(); child; child = child->getNextNode() )
- disableTabFocusTree( child );
-}
-
SettingsPanel::SettingsPanel( App* app ) :
mApp( app ), mLifetime( this, app ? app->getUISceneNode() : nullptr ) {}
@@ -316,161 +18,70 @@ SettingsPanel::PanelState& SettingsPanel::state( Scope scope ) {
}
void SettingsPanel::PanelState::reset() {
- bindingGroup.clear();
connections.clear();
window = nullptr;
- search = nullptr;
- categories = nullptr;
- scroll = nullptr;
- settings = nullptr;
- pageTitle = nullptr;
- categoryModel.reset();
- categoryItems.clear();
- categoryIds.clear();
- categorySearchText.clear();
- categoryTitles.clear();
- categoryHeadings.clear();
- categorySections.clear();
- categoryContainers.clear();
- materializedCategories.clear();
- subcategoryHeadings.clear();
- model.clear();
+ panel = nullptr;
documents.clear();
- settingViews.clear();
- selectedCategory.clear();
- categoryFilter.clear();
}
void SettingsPanel::show( Scope scope, const std::string& category ) {
- auto& panel = state( scope );
- if ( panel.window ) {
- panel.window->show();
- panel.window->toFront();
- selectCategory( panel, category );
- panel.search->runOnMainThread( [search = panel.search] { search->setFocus(); } );
+ auto& state = this->state( scope );
+ if ( state.window ) {
+ state.window->show();
+ state.window->toFront();
+ selectCategory( state, category );
+ state.panel->runOnMainThread( [panel = state.panel] { panel->focusSearch(); } );
return;
}
- Clock c;
+ Clock clock;
create( scope );
- selectCategory( panel, category );
+ selectCategory( state, category );
Log::info( "Settings Panel %s created in %s", scope == Scope::User ? "User" : "Project",
- c.getElapsedTime().toString() );
-}
-
-void SettingsPanel::selectCategory( PanelState& panel, const std::string& category ) {
- if ( category.empty() || !panel.categories )
- return;
- String title;
- if ( String::endsWith( category, ".*" ) ) {
- const std::string prefix = category.substr( 0, category.size() - 1 );
- for ( const auto& [parent, children] : panel.categoryItems ) {
- if ( !children.empty() ) {
- auto id = panel.categoryIds.find( parent + '/' + children.front() );
- if ( id != panel.categoryIds.end() && String::startsWith( id->second, prefix ) ) {
- title = String::fromUtf8( parent );
- break;
- }
- }
- }
- } else if ( auto found = panel.categoryTitles.find( category );
- found != panel.categoryTitles.end() ) {
- title = found->second;
- }
- if ( title.empty() )
- return;
- panel.selectedCategory = category;
- auto index = panel.categories->findRowWithText(
- title.toUtf8(), true, UIAbstractView::FindRowWithTextMatchKind::Equals );
- if ( index.isValid() )
- panel.categories->setSelection( index );
- filter( panel );
- panel.scroll->getVerticalScrollBar()->setValue( 0, false );
+ clock.getElapsedTime().toString() );
}
void SettingsPanel::create( Scope scope ) {
- auto& panel = state( scope );
+ auto& state = this->state( scope );
UIWindow::StyleConfig config{ UI_WIN_DEFAULT_FLAGS | UI_WIN_MAXIMIZE_BUTTON | UI_WIN_MODAL };
- panel.window = UIWindow::NewOpt( UIWindow::SIMPLE_LAYOUT, config );
- panel.window->setId( scope == Scope::User ? "settings_panel" : "project_settings_panel" );
- panel.window->setTitle( scope == Scope::User
+ state.window = UIWindow::NewOpt( UIWindow::SIMPLE_LAYOUT, config );
+ state.window->setId( scope == Scope::User ? "settings_panel" : "project_settings_panel" );
+ state.window->setTitle( scope == Scope::User
? mApp->i18n( "settings", "Settings" )
: mApp->i18n( "project_settings", "Project Settings" ) );
const auto sceneSize = mApp->getUISceneNode()->getPixelsSize();
- panel.window->setPixelsSize( { eeclamp( sceneSize.getWidth() * 0.82f, 720.f, 1200.f ),
+ state.window->setPixelsSize( { eeclamp( sceneSize.getWidth() * 0.82f, 720.f, 1200.f ),
eeclamp( sceneSize.getHeight() * 0.82f, 520.f, 850.f ) } );
- panel.window->setMinWindowSize( 640, 440 );
- panel.window->setKeyBindingCommand( "closeWindow", [window = panel.window, this] {
+ state.window->setMinWindowSize( 640, 440 );
+ state.window->setKeyBindingCommand( "closeWindow", [window = state.window, this] {
if ( !SceneManager::instance()->isShuttingDown() ) {
window->closeWindow();
if ( mApp->getSplitter() && mApp->getSplitter()->getCurWidget() )
mApp->getSplitter()->getCurWidget()->setFocus();
}
} );
- panel.window->getKeyBindings().addKeybind( { KEY_ESCAPE }, "closeWindow" );
- auto* layout = mApp->getUISceneNode()->loadLayoutFromString( SETTINGS_PANEL_LAYOUT,
- panel.window->getContainer() );
- panel.search = layout->find( "settings_filter" );
- panel.categories = layout->find( "settings_categories" );
- panel.settings = layout->find( "settings_rows" );
- panel.pageTitle = layout->find( "settings_page_title" );
- panel.scroll = layout->find( "settings_scroll" );
- panel.scroll->setVerticalScrollMode( ScrollBarMode::Auto );
- panel.scroll->setHorizontalScrollMode( ScrollBarMode::AlwaysOff );
- disableTabFocusTree( panel.categories->getVerticalScrollBar() );
- disableTabFocusTree( panel.categories->getHorizontalScrollBar() );
- disableTabFocusTree( panel.scroll->getVerticalScrollBar() );
- disableTabFocusTree( panel.scroll->getHorizontalScrollBar() );
- panel.window->setKeyBindingCommand( "focusSettingsFilter", [&panel] {
- panel.search->setFocus();
- panel.search->getDocument().selectAll();
- } );
- panel.window->setKeyBindingCommand( "focusSettingsCategories", [&panel] {
- auto selected = panel.categories->getSelection().first();
- if ( selected.isValid() )
- panel.categories->setSelection( selected );
- panel.categories->setFocus();
- } );
- panel.window->getKeyBindings().addKeybind( { KEY_F, KeyMod::getDefaultModifier() },
+ state.window->getKeyBindings().addKeybind( { KEY_ESCAPE }, "closeWindow" );
+ state.panel = UISettingsPanel::New( state.window->getContainer() );
+ state.panel->setSearchResultsText( mApp->i18n( "search_results", "Search Results" ) );
+ state.window->setKeyBindingCommand( "focusSettingsFilter",
+ [panel = state.panel] { panel->focusSearch(); } );
+ state.window->setKeyBindingCommand( "focusSettingsCategories",
+ [panel = state.panel] { panel->focusCategories(); } );
+ state.window->getKeyBindings().addKeybind( { KEY_F, KeyMod::getDefaultModifier() },
"focusSettingsFilter" );
- panel.window->getKeyBindings().addKeybind(
+ state.window->getKeyBindings().addKeybind(
{ KEY_E, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "focusSettingsCategories" );
- panel.settings->beginAttributesTransaction();
if ( scope == Scope::User ) {
- addUserSettings( panel );
- addPluginSettings( panel );
+ addUserSettings( state );
+ addPluginSettings( state );
} else {
- addProjectSettings( panel );
+ addProjectSettings( state );
}
- materializeCategory( panel, panel.selectedCategory );
- setupCategories( panel );
- panel.settings->endAttributesTransaction();
- filter( panel );
- panel.connections +=
- panel.search->connect( Event::OnTextChanged, [this, &panel]( const Event* ) {
- String query = panel.search->getText();
- query.trim();
- const UintPtr debounceTag = reinterpret_cast( &panel );
- if ( query.size() < 2 ) {
- panel.search->removeActionsByTag( debounceTag );
- filter( panel );
- return;
- }
- panel.search->debounce( [this, &panel] { filter( panel ); }, Milliseconds( 150 ),
- debounceTag );
- } );
- panel.connections += panel.window->connect( Event::OnWindowReady, [&panel]( const Event* ) {
- auto title = panel.categoryTitles.find( panel.selectedCategory );
- if ( title != panel.categoryTitles.end() ) {
- auto index = panel.categories->findRowWithText(
- title->second.toUtf8(), true, UIAbstractView::FindRowWithTextMatchKind::Equals );
- if ( index.isValid() )
- panel.categories->setSelection( index );
- }
- panel.search->runOnMainThread( [search = panel.search] { search->setFocus(); } );
- } );
- panel.connections +=
- panel.window->connect( Event::OnWindowClose, [this, &panel, scope]( const Event* ) {
- for ( const auto& document : panel.documents ) {
+ state.panel->build();
+ state.connections += state.window->connect(
+ Event::OnWindowReady, [&state]( const Event* ) { state.panel->focusSearch(); } );
+ state.connections +=
+ state.window->connect( Event::OnWindowClose, [this, &state, scope]( const Event* ) {
+ for ( const auto& document : state.documents ) {
if ( !document->save() )
Log::error( "Could not save settings document: %s", document->path() );
}
@@ -478,368 +89,84 @@ void SettingsPanel::create( Scope scope ) {
mApp->saveConfig();
else
mApp->saveProject();
- panel.reset();
+ state.reset();
} );
- panel.window->center();
- panel.window->showWhenReady();
- panel.search->setFocus();
+ state.window->center();
+ state.window->showWhenReady();
+ state.panel->focusSearch();
}
-void SettingsPanel::addCategory( PanelState& panel, const std::string& id, const String& parent,
- const String& name ) {
- panel.model.addCategory( { id, parent, name } );
- const auto parentText = parent.toUtf8();
- const auto nameText = name.toUtf8();
- auto parentItems =
- std::find_if( panel.categoryItems.begin(), panel.categoryItems.end(),
- [&parentText]( const auto& item ) { return item.first == parentText; } );
- if ( parentItems == panel.categoryItems.end() ) {
- panel.categoryItems.emplace_back( parentText, std::vector{ nameText } );
- } else {
- parentItems->second.emplace_back( nameText );
- }
- panel.categoryIds[parentText + '/' + nameText] = id;
- panel.categorySearchText[id] = parent + " " + name;
- panel.categoryTitles[id] = name;
- auto* section = mApp->getUISceneNode()->loadLayoutFromString( SETTINGS_CATEGORY_HEADING_LAYOUT,
- panel.settings );
- auto* heading = section->find( "settings_category_heading" );
- heading->setText( name );
- heading->setId( "settings_category_" + id );
- panel.categoryHeadings[id] = heading;
- panel.categorySections[id] = section;
- panel.categoryContainers[id] = section->find( "settings_category_rows" );
- if ( panel.selectedCategory.empty() )
- panel.selectedCategory = id;
+void SettingsPanel::selectCategory( PanelState& state, const std::string& category ) {
+ state.panel->selectCategory( category );
}
-void SettingsPanel::addSubcategoryHeading( PanelState& panel, const std::string& category,
- const String& name ) {
- panel.model.addGroup( { category, name, panel.model.settings().size() } );
+void SettingsPanel::addCategory( PanelState& state, std::string id, String parent, String name ) {
+ state.panel->addCategory( std::move( id ), std::move( parent ), std::move( name ) );
}
-void SettingsPanel::setupCategories( PanelState& panel ) {
- auto model = SettingsCategoryModel::create( panel.categoryItems, panel.categoryIds );
- panel.categoryModel = model;
- panel.categories->setHeadersVisible( false );
- panel.categories->setAutoExpandOnSingleColumn( true );
- panel.categories->setFocusOnSelection( true );
- panel.categories->setModel( model );
- panel.categories->expandAll();
- panel.connections +=
- panel.categories->connect( Event::OnSelectionChanged, [this, &panel]( const Event* ) {
- auto index = panel.categories->getSelection().first();
- if ( !index.isValid() )
- return;
- auto* node = static_cast( index.internalData() );
- if ( !node || node->id.empty() )
- return;
- panel.selectedCategory = node->id;
- panel.pageTitle->setText( node->text );
- filter( panel );
- panel.scroll->getVerticalScrollBar()->setValue( 0, false );
- } );
+void SettingsPanel::addSubcategoryHeading( PanelState& state, std::string category, String name ) {
+ state.panel->addGroup( std::move( category ), std::move( name ) );
}
-UIWidget* SettingsPanel::createRow( PanelState& panel, SettingDefinition& setting,
- SettingView& view, pugi::xml_node layout ) {
- auto& binding = setting.descriptor;
- auto container = panel.categoryContainers.find( binding.category );
- eeASSERT( container != panel.categoryContainers.end() );
- auto* row = mApp->getUISceneNode()->loadLayoutNodes( layout, container->second, 0 );
- row->setId( "setting_" + binding.id );
- row->find( "setting_name" )->setText( binding.name );
- auto* description = row->find( "setting_description" );
- description->setText( binding.description );
- view.row = row;
- return row;
-}
-
-void SettingsPanel::addBool( PanelState& panel, SettingDescriptor binding, bool* value,
+void SettingsPanel::addBool( PanelState& state, SettingDescriptor binding, bool* value,
std::function apply ) {
- panel.model.addSetting(
- { std::move( binding ), BoolPointerSetting{ value, std::move( apply ) } } );
+ state.panel->addBool( std::move( binding ), value, std::move( apply ) );
}
-void SettingsPanel::addBool( PanelState& panel, SettingDescriptor binding,
+void SettingsPanel::addBool( PanelState& state, SettingDescriptor binding,
std::function get, std::function set ) {
- panel.model.addSetting(
- { std::move( binding ), BoolSetting{ std::move( get ), std::move( set ) } } );
+ state.panel->addBool( std::move( binding ), std::move( get ), std::move( set ) );
}
-UICheckBox* SettingsPanel::createBoolControl( PanelState& panel, SettingDefinition& setting,
- SettingView& view ) {
- auto* row = createRow( panel, setting, view, SETTINGS_BOOL_ROW_LAYOUT.root() );
- row->addClass( "settings_boolean_option" );
- auto* check = row->find( "setting_control_widget" );
- auto toggle = [check]( const Event* event ) {
- if ( event->asMouseEvent()->getFlags() & EE_BUTTON_LMASK )
- check->setChecked( !check->isChecked() );
- };
- panel.connections +=
- row->find