ecode: decouple and lazily render settings panel entries

- introduce typed, widget-independent settings descriptors
  - separate settings registration from UI materialization
  - construct setting rows only for visible categories and search results
  - preserve category, aggregate view, subgroup, and disabled-state behavior
  - validate duplicate IDs and unknown category references
  - debounce settings filtering and ignore single-character searches
  - avoid temporary allocations while matching category names
This commit is contained in:
Martín Lucas Golini
2026-08-25 23:40:19 -03:00
parent 6b0646ab48
commit 42d9dd6473
3 changed files with 446 additions and 156 deletions

View File

@@ -0,0 +1,144 @@
#ifndef ECODE_SETTINGSMODEL_HPP
#define ECODE_SETTINGSMODEL_HPP
#include <algorithm>
#include <eepp/ee.hpp>
#include <functional>
#include <string>
#include <variant>
#include <vector>
namespace ecode {
struct SettingDescriptor {
std::string id;
std::string category;
String name;
String description;
String group;
};
struct BoolPointerSetting {
bool* value{ nullptr };
std::function<void( bool )> apply;
};
struct BoolSetting {
std::function<bool()> get;
std::function<void( bool )> set;
};
struct ChoiceSetting {
std::vector<String> choices;
std::vector<String> descriptions;
std::function<size_t()> get;
std::function<void( size_t )> set;
};
struct EditableChoiceSetting {
std::vector<String> choices;
std::function<String()> get;
std::function<bool( const String& )> set;
};
struct IntegerSetting {
int min{ 0 };
int max{ 0 };
std::function<int()> get;
std::function<void( int )> set;
};
struct TextSetting {
std::function<std::string()> get;
std::function<bool( const std::string& )> set;
bool commitOnFocusLoss{ false };
};
struct FloatSetting {
double min{ 0 };
double max{ 0 };
double step{ 0 };
std::function<double()> get;
std::function<void( double )> set;
};
struct ActionSetting {
String buttonText;
std::function<void()> action;
};
using SettingValue =
std::variant<BoolPointerSetting, BoolSetting, ChoiceSetting, EditableChoiceSetting,
IntegerSetting, TextSetting, FloatSetting, ActionSetting>;
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<SettingsCategory>& categories() const { return mCategories; }
const std::vector<SettingsGroup>& groups() const { return mGroups; }
std::vector<SettingDefinition>& settings() { return mSettings; }
const std::vector<SettingDefinition>& settings() const { return mSettings; }
private:
std::vector<SettingsCategory> mCategories;
std::vector<SettingsGroup> mGroups;
std::vector<SettingDefinition> mSettings;
};
} // namespace ecode
#endif

View File

@@ -273,7 +273,10 @@ class SettingsLayoutTemplate {
};
static constexpr const char* SETTINGS_CATEGORY_HEADING_LAYOUT = R"xml(
<TextView lw="mp" lh="wc" class="settings_category_heading" visible="false" focusable="false" />
<vbox lw="mp" lh="wc" visible="false">
<TextView id="settings_category_heading" lw="mp" lh="wc" class="settings_category_heading" visible="false" focusable="false" />
<vbox id="settings_category_rows" lw="mp" lh="wc" />
</vbox>
)xml";
static constexpr const char* SETTINGS_SUBCATEGORY_HEADING_LAYOUT = R"xml(
<TextView lw="mp" lh="wc" class="settings_subcategory_heading" visible="false" focusable="false" />
@@ -319,8 +322,12 @@ void SettingsPanel::PanelState::reset() {
categorySearchText.clear();
categoryTitles.clear();
categoryHeadings.clear();
categorySections.clear();
categoryContainers.clear();
materializedCategories.clear();
subcategoryHeadings.clear();
bindings.clear();
model.clear();
settingViews.clear();
selectedCategory.clear();
categoryFilter.clear();
}
@@ -423,11 +430,23 @@ void SettingsPanel::create( Scope scope ) {
addUserSettings( panel );
else
addProjectSettings( panel );
materializeCategory( panel, panel.selectedCategory );
setupCategories( panel );
panel.settings->endAttributesTransaction();
filter( panel );
panel.connections += panel.search->connect(
Event::OnTextChanged, [this, &panel]( const Event* ) { 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<UintPtr>( &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() ) {
@@ -453,6 +472,7 @@ void SettingsPanel::create( Scope scope ) {
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 =
@@ -466,24 +486,22 @@ void SettingsPanel::addCategory( PanelState& panel, const std::string& id, const
panel.categoryIds[parentText + '/' + nameText] = id;
panel.categorySearchText[id] = parent + " " + name;
panel.categoryTitles[id] = name;
auto* heading = mApp->getUISceneNode()
->loadLayoutFromString( SETTINGS_CATEGORY_HEADING_LAYOUT, panel.settings )
->asType<UITextView>();
auto* section = mApp->getUISceneNode()->loadLayoutFromString( SETTINGS_CATEGORY_HEADING_LAYOUT,
panel.settings );
auto* heading = section->find<UITextView>( "settings_category_heading" );
heading->setText( name );
heading->setId( "settings_category_" + id );
panel.categoryHeadings[id] = heading;
panel.categorySections[id] = section;
panel.categoryContainers[id] = section->find<UILinearLayout>( "settings_category_rows" );
if ( panel.selectedCategory.empty() )
panel.selectedCategory = id;
}
void SettingsPanel::addSubcategoryHeading( PanelState& panel, const std::string& category,
const String& name ) {
auto* heading =
mApp->getUISceneNode()
->loadLayoutFromString( SETTINGS_SUBCATEGORY_HEADING_LAYOUT, panel.settings )
->asType<UITextView>();
heading->setText( name );
panel.subcategoryHeadings.push_back( { category, name, heading } );
panel.model.addGroup( { category, name, panel.model.settings().size() } );
panel.subcategoryHeadings.push_back( { category, name, nullptr } );
}
void SettingsPanel::setupCategories( PanelState& panel ) {
@@ -509,40 +527,35 @@ void SettingsPanel::setupCategories( PanelState& panel ) {
} );
}
UIWidget* SettingsPanel::createRow( PanelState& panel, SettingBinding& binding,
pugi::xml_node layout ) {
auto* row = mApp->getUISceneNode()->loadLayoutNodes( layout, panel.settings, 0 );
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<UITextView>( "setting_name" )->setText( binding.name );
auto* description = row->find<UITextView>( "setting_description" );
description->setText( binding.description );
binding.row = row;
view.row = row;
return row;
}
void SettingsPanel::addBool( PanelState& panel, SettingBinding binding, bool* value,
void SettingsPanel::addBool( PanelState& panel, SettingDescriptor binding, bool* value,
std::function<void( bool )> apply ) {
auto* check = createBoolControl( panel, binding );
auto valueBinding =
UIDataBind<bool>::New( value, check, UIValueConverter<bool>::converterBool() );
valueBinding->onValueChangeCb = std::move( apply );
panel.bindingGroup += std::move( valueBinding );
panel.bindings.emplace_back( std::move( binding ) );
panel.model.addSetting(
{ std::move( binding ), BoolPointerSetting{ value, std::move( apply ) } } );
}
void SettingsPanel::addBool( PanelState& panel, SettingBinding binding, std::function<bool()> get,
std::function<void( bool )> set ) {
auto* check = createBoolControl( panel, binding );
check->setChecked( get() );
panel.connections +=
check->connect( Event::OnValueChange, [check, set = std::move( set )]( const Event* ) {
set( check->isChecked() );
} );
panel.bindings.emplace_back( std::move( binding ) );
void SettingsPanel::addBool( PanelState& panel, SettingDescriptor binding,
std::function<bool()> get, std::function<void( bool )> set ) {
panel.model.addSetting(
{ std::move( binding ), BoolSetting{ std::move( get ), std::move( set ) } } );
}
UICheckBox* SettingsPanel::createBoolControl( PanelState& panel, SettingBinding& binding ) {
auto* row = createRow( panel, binding, SETTINGS_BOOL_ROW_LAYOUT.root() );
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<UICheckBox>( "setting_control_widget" );
auto toggle = [check]( const Event* ) { check->setChecked( !check->isChecked() ); };
@@ -555,121 +568,217 @@ UICheckBox* SettingsPanel::createBoolControl( PanelState& panel, SettingBinding&
return check;
}
void SettingsPanel::addChoice( PanelState& panel, SettingBinding binding,
void SettingsPanel::addChoice( PanelState& panel, SettingDescriptor binding,
const std::vector<String>& choices, std::function<size_t()> get,
std::function<void( size_t )> set,
std::vector<String> choiceDescriptions ) {
auto* row = createRow( panel, binding, SETTINGS_CHOICE_ROW_LAYOUT.root() );
auto* dropDown = row->find<UIDropDownModelList>( "setting_control_widget" );
auto model = ItemListOwnerModel<String>::create( choices );
dropDown->setModel( model );
const size_t selected = get();
if ( selected < choices.size() ) {
dropDown->getListView()->getSelection().set( model->index( selected, 0 ) );
dropDown->setText( choices[selected] );
}
if ( selected < choiceDescriptions.size() )
dropDown->setTooltipText( choiceDescriptions[selected] );
panel.connections += dropDown->connect(
Event::OnValueChange, [dropDown, set = std::move( set ),
descriptions = std::move( choiceDescriptions )]( const Event* ) {
if ( dropDown->getListView()->getSelection().isEmpty() )
return;
const size_t selected = dropDown->getListView()->getSelection().first().row();
if ( selected < descriptions.size() )
dropDown->setTooltipText( descriptions[selected] );
set( selected );
} );
panel.bindings.emplace_back( std::move( binding ) );
panel.model.addSetting(
{ std::move( binding ), ChoiceSetting{ choices, std::move( choiceDescriptions ),
std::move( get ), std::move( set ) } } );
}
void SettingsPanel::addEditableChoice( PanelState& panel, SettingBinding binding,
void SettingsPanel::addEditableChoice( PanelState& panel, SettingDescriptor binding,
const std::vector<String>& choices,
std::function<String()> get,
std::function<bool( const String& )> set ) {
auto* row = createRow( panel, binding, SETTINGS_EDITABLE_CHOICE_ROW_LAYOUT.root() );
auto* combo = row->find<UIComboBox>( "setting_control_widget" );
for ( const auto& choice : choices )
combo->getListBox()->addListBoxItem( choice );
combo->setText( get() );
panel.connections +=
combo->connect( Event::OnValueChange, [combo, set = std::move( set )]( const Event* ) {
if ( !set( combo->getText() ) ) {
combo->addClass( "error" );
combo->getDropDownList()->addClass( "error" );
return;
}
combo->removeClass( "error" );
combo->getDropDownList()->removeClass( "error" );
} );
panel.bindings.emplace_back( std::move( binding ) );
panel.model.addSetting(
{ std::move( binding ),
EditableChoiceSetting{ choices, std::move( get ), std::move( set ) } } );
}
void SettingsPanel::addInteger( PanelState& panel, SettingBinding binding, int min, int max,
void SettingsPanel::addInteger( PanelState& panel, SettingDescriptor binding, int min, int max,
std::function<int()> get, std::function<void( int )> set ) {
auto* row = createRow( panel, binding, SETTINGS_INTEGER_ROW_LAYOUT.root() );
auto* spin = row->find<UISpinBox>( "setting_control_widget" );
spin->setMinValue( min )->setMaxValue( max );
spin->unsetTabFocusable();
spin->getButtonPushUp()->asType<UIWidget>()->unsetTabFocusable();
spin->getButtonPushDown()->asType<UIWidget>()->unsetTabFocusable();
spin->setValue( get() );
panel.connections +=
spin->connect( Event::OnValueChange, [spin, set = std::move( set )]( const Event* ) {
set( static_cast<int>( spin->getValue() ) );
} );
panel.bindings.emplace_back( std::move( binding ) );
panel.model.addSetting(
{ std::move( binding ), IntegerSetting{ min, max, std::move( get ), std::move( set ) } } );
}
void SettingsPanel::addText( PanelState& panel, SettingBinding binding,
void SettingsPanel::addText( PanelState& panel, SettingDescriptor binding,
std::function<std::string()> get,
std::function<bool( const std::string& )> set,
bool commitOnFocusLoss ) {
auto* row = createRow( panel, binding, SETTINGS_TEXT_ROW_LAYOUT.root() );
auto* input = row->find<UITextInput>( "setting_control_widget" );
input->setText( String::fromUtf8( get() ) );
auto commit = [input, set = std::move( set )]( const Event* ) {
std::string text = input->getText().toUtf8();
if ( !set( text ) ) {
input->addClass( "error" );
return;
}
input->removeClass( "error" );
};
if ( commitOnFocusLoss ) {
panel.connections += input->connect( Event::OnPressEnter, commit );
panel.connections += input->connect( Event::OnFocusLoss, std::move( commit ) );
} else {
panel.connections += input->connect( Event::OnTextChanged, std::move( commit ) );
}
panel.bindings.emplace_back( std::move( binding ) );
panel.model.addSetting( { std::move( binding ), TextSetting{ std::move( get ), std::move( set ),
commitOnFocusLoss } } );
}
void SettingsPanel::addFloat( PanelState& panel, SettingBinding binding, double min, double max,
void SettingsPanel::addFloat( PanelState& panel, SettingDescriptor binding, double min, double max,
double step, std::function<double()> get,
std::function<void( double )> set ) {
auto* row = createRow( panel, binding, SETTINGS_INTEGER_ROW_LAYOUT.root() );
auto* spin = row->find<UISpinBox>( "setting_control_widget" );
spin->setMinValue( min )->setMaxValue( max )->setClickStep( step );
spin->allowFloatingPoint( true )->setValue( get() );
spin->unsetTabFocusable();
spin->getButtonPushUp()->asType<UIWidget>()->unsetTabFocusable();
spin->getButtonPushDown()->asType<UIWidget>()->unsetTabFocusable();
panel.connections +=
spin->connect( Event::OnValueChange, [spin, set = std::move( set )]( const Event* ) {
set( spin->getValue() );
} );
panel.bindings.emplace_back( std::move( binding ) );
panel.model.addSetting( { std::move( binding ), FloatSetting{ min, max, step, std::move( get ),
std::move( set ) } } );
}
void SettingsPanel::addAction( PanelState& panel, SettingBinding binding, const String& buttonText,
std::function<void()> action ) {
auto* row = createRow( panel, binding, SETTINGS_ACTION_ROW_LAYOUT.root() );
auto* button = row->find<UIPushButton>( "setting_control_widget" );
button->setText( buttonText );
panel.connections += button->connect(
Event::MouseClick, [action = std::move( action )]( const Event* ) { action(); } );
panel.bindings.emplace_back( std::move( binding ) );
void SettingsPanel::addAction( PanelState& panel, SettingDescriptor binding,
const String& buttonText, std::function<void()> action ) {
panel.model.addSetting(
{ std::move( binding ), ActionSetting{ buttonText, std::move( action ) } } );
}
static void setNodeTreeEnabled( Node* node, bool enabled );
void SettingsPanel::materializeCategory( PanelState& 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 =
mApp->getUISceneNode()
->loadLayoutFromString( SETTINGS_SUBCATEGORY_HEADING_LAYOUT, container->second )
->asType<UITextView>();
heading->setText( group.name );
auto subcategory =
std::find_if( panel.subcategoryHeadings.begin(), panel.subcategoryHeadings.end(),
[&group]( const auto& item ) {
return item.category == group.category &&
item.name == group.name && !item.heading;
} );
if ( subcategory != panel.subcategoryHeadings.end() )
subcategory->heading = heading;
}
auto& view = panel.settingViews[i];
if ( auto* value = std::get_if<BoolPointerSetting>( &setting.value ) ) {
auto* check = createBoolControl( panel, setting, view );
auto binding = UIDataBind<bool>::New( value->value, check,
UIValueConverter<bool>::converterBool() );
binding->onValueChangeCb = value->apply;
panel.bindingGroup += std::move( binding );
} else if ( auto* value = std::get_if<BoolSetting>( &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<ChoiceSetting>( &setting.value ) ) {
auto* row = createRow( panel, setting, view, SETTINGS_CHOICE_ROW_LAYOUT.root() );
auto* dropDown = row->find<UIDropDownModelList>( "setting_control_widget" );
auto model = ItemListOwnerModel<String>::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<EditableChoiceSetting>( &setting.value ) ) {
auto* row =
createRow( panel, setting, view, SETTINGS_EDITABLE_CHOICE_ROW_LAYOUT.root() );
auto* combo = row->find<UIComboBox>( "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<IntegerSetting>( &setting.value ) ) {
auto* row = createRow( panel, setting, view, SETTINGS_INTEGER_ROW_LAYOUT.root() );
auto* spin = row->find<UISpinBox>( "setting_control_widget" );
spin->setMinValue( value->min )->setMaxValue( value->max );
spin->unsetTabFocusable();
spin->getButtonPushUp()->asType<UIWidget>()->unsetTabFocusable();
spin->getButtonPushDown()->asType<UIWidget>()->unsetTabFocusable();
spin->setValue( value->get() );
panel.connections +=
spin->connect( Event::OnValueChange, [spin, value]( const Event* ) {
value->set( static_cast<int>( spin->getValue() ) );
} );
} else if ( auto* value = std::get_if<TextSetting>( &setting.value ) ) {
auto* row = createRow( panel, setting, view, SETTINGS_TEXT_ROW_LAYOUT.root() );
auto* input = row->find<UITextInput>( "setting_control_widget" );
input->setText( String::fromUtf8( value->get() ) );
auto commit = [input, value]( const Event* ) {
if ( !value->set( input->getText().toUtf8() ) ) {
input->addClass( "error" );
return;
}
input->removeClass( "error" );
};
if ( value->commitOnFocusLoss ) {
panel.connections += input->connect( Event::OnPressEnter, commit );
panel.connections += input->connect( Event::OnFocusLoss, std::move( commit ) );
} else {
panel.connections += input->connect( Event::OnTextChanged, std::move( commit ) );
}
} else if ( auto* value = std::get_if<FloatSetting>( &setting.value ) ) {
auto* row = createRow( panel, setting, view, SETTINGS_INTEGER_ROW_LAYOUT.root() );
auto* spin = row->find<UISpinBox>( "setting_control_widget" );
spin->setMinValue( value->min )->setMaxValue( value->max )->setClickStep( value->step );
spin->allowFloatingPoint( true )->setValue( value->get() );
spin->unsetTabFocusable();
spin->getButtonPushUp()->asType<UIWidget>()->unsetTabFocusable();
spin->getButtonPushDown()->asType<UIWidget>()->unsetTabFocusable();
panel.connections +=
spin->connect( Event::OnValueChange,
[spin, value]( const Event* ) { value->set( spin->getValue() ); } );
} else if ( auto* value = std::get_if<ActionSetting>( &setting.value ) ) {
auto* row = createRow( panel, setting, view, SETTINGS_ACTION_ROW_LAYOUT.root() );
auto* button = row->find<UIPushButton>( "setting_control_widget" );
button->setText( value->buttonText );
panel.connections +=
button->connect( Event::MouseClick, [value]( const Event* ) { value->action(); } );
}
if ( view.row && !setting.enabled )
setNodeTreeEnabled( view.row, false );
}
container->second->endAttributesTransaction();
panel.materializedCategories.insert( category );
}
void SettingsPanel::materializeVisibleSettings( PanelState& 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 ) {
@@ -703,9 +812,15 @@ static bool parseNonNegativeTime( const std::string& text, Time& time ) {
void SettingsPanel::setCategoryEnabled( PanelState& panel, const std::string& category,
bool enabled, const std::string& excludedSetting ) {
for ( auto& binding : panel.bindings ) {
if ( binding.category == category && binding.id != excludedSetting && binding.row )
setNodeTreeEnabled( binding.row, enabled );
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 );
}
}
@@ -1346,7 +1461,7 @@ void SettingsPanel::addUserSettings( PanelState& panel ) {
mApp->i18n( "fallback_font_desc", "Choose the font used for missing glyphs." ) },
mApp->i18n( "choose_font", "Choose Font..." ),
[this] { mApp->runCommand( "fallback-font" ); } );
auto addFontSize = [this, &panel]( SettingBinding binding, std::function<std::string()> get,
auto addFontSize = [this, &panel]( SettingDescriptor binding, std::function<std::string()> get,
std::function<void( const StyleSheetLength& )> set ) {
addText(
panel, std::move( binding ), std::move( get ),
@@ -2063,6 +2178,9 @@ void SettingsPanel::addProjectSettings( PanelState& panel ) {
void SettingsPanel::filter( PanelState& 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( mApp->i18n( "search_results", "Search Results" ) );
} else if ( auto title = panel.categoryTitles.find( panel.selectedCategory );
@@ -2076,7 +2194,8 @@ void SettingsPanel::filter( PanelState& panel ) {
aggregate ? panel.selectedCategory.substr( 0, panel.selectedCategory.size() - 1 )
: std::string{};
if ( !query.empty() ) {
for ( const auto& binding : panel.bindings ) {
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 ) ||
@@ -2093,7 +2212,9 @@ void SettingsPanel::filter( PanelState& panel ) {
for ( auto& [category, heading] : panel.categoryHeadings )
heading->setVisible( query.empty() && aggregate &&
String::startsWith( category, aggregatePrefix ) );
for ( auto& binding : panel.bindings ) {
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 );
@@ -2105,13 +2226,33 @@ void SettingsPanel::filter( PanelState& panel ) {
String::icontains( binding.description, query ) ||
String::icontains( binding.group, query ) ||
String::icontains( binding.id, queryUtf8 );
binding.row->setVisible( matches );
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(
panel.bindings.begin(), panel.bindings.end(), [&subcategory]( const auto& binding ) {
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 && binding.row && binding.row->isVisible();
binding.group == subcategory.name && index < panel.settingViews.size() &&
panel.settingViews[index].row && panel.settingViews[index].row->isVisible();
} );
subcategory.heading->setVisible( hasVisibleSetting );
}

View File

@@ -1,6 +1,7 @@
#ifndef ECODE_SETTINGSPANEL_HPP
#define ECODE_SETTINGSPANEL_HPP
#include "settingsmodel.hpp"
#include <eepp/ee.hpp>
#include <functional>
#include <string>
@@ -19,12 +20,7 @@ class SettingsPanel {
void show( Scope scope, const std::string& category = {} );
protected:
struct SettingBinding {
std::string id;
std::string category;
String name;
String description;
String group;
struct SettingView {
UIWidget* row{ nullptr };
};
struct SubcategoryHeading {
@@ -43,13 +39,17 @@ class SettingsPanel {
UITextView* pageTitle{ nullptr };
std::shared_ptr<Model> categoryModel;
UIBindingGroup bindingGroup;
SettingsModel model;
std::vector<std::pair<std::string, std::vector<std::string>>> categoryItems;
UnorderedMap<std::string, std::string> categoryIds;
UnorderedMap<std::string, String> categorySearchText;
UnorderedMap<std::string, String> categoryTitles;
UnorderedMap<std::string, UITextView*> categoryHeadings;
UnorderedMap<std::string, UIWidget*> categorySections;
UnorderedMap<std::string, UILinearLayout*> categoryContainers;
UnorderedSet<std::string> materializedCategories;
std::vector<SubcategoryHeading> subcategoryHeadings;
std::vector<SettingBinding> bindings;
std::vector<SettingView> settingViews;
std::string selectedCategory;
std::string categoryFilter;
@@ -70,26 +70,31 @@ class SettingsPanel {
void addSubcategoryHeading( PanelState& state, const std::string& category,
const String& name );
void setupCategories( PanelState& state );
void addBool( PanelState& state, SettingBinding binding, bool* value,
void addBool( PanelState& state, SettingDescriptor binding, bool* value,
std::function<void( bool )> apply = {} );
void addBool( PanelState& state, SettingBinding binding, std::function<bool()> get,
void addBool( PanelState& state, SettingDescriptor binding, std::function<bool()> get,
std::function<void( bool )> set );
void addChoice( PanelState& state, SettingBinding binding, const std::vector<String>& choices,
std::function<size_t()> get, std::function<void( size_t )> set,
void addChoice( PanelState& state, SettingDescriptor binding,
const std::vector<String>& choices, std::function<size_t()> get,
std::function<void( size_t )> set,
std::vector<String> choiceDescriptions = {} );
void addEditableChoice( PanelState& state, SettingBinding binding,
void addEditableChoice( PanelState& state, SettingDescriptor binding,
const std::vector<String>& choices, std::function<String()> get,
std::function<bool( const String& )> set );
void addInteger( PanelState& state, SettingBinding binding, int min, int max,
void addInteger( PanelState& state, SettingDescriptor binding, int min, int max,
std::function<int()> get, std::function<void( int )> set );
void addText( PanelState& state, SettingBinding binding, std::function<std::string()> get,
void addText( PanelState& state, SettingDescriptor binding, std::function<std::string()> get,
std::function<bool( const std::string& )> set, bool commitOnFocusLoss = false );
void addFloat( PanelState& state, SettingBinding binding, double min, double max, double step,
std::function<double()> get, std::function<void( double )> set );
void addAction( PanelState& state, SettingBinding binding, const String& buttonText,
void addFloat( PanelState& state, SettingDescriptor binding, double min, double max,
double step, std::function<double()> get, std::function<void( double )> set );
void addAction( PanelState& state, SettingDescriptor binding, const String& buttonText,
std::function<void()> action );
UIWidget* createRow( PanelState& state, SettingBinding& binding, pugi::xml_node layout );
UICheckBox* createBoolControl( PanelState& state, SettingBinding& binding );
UIWidget* createRow( PanelState& state, SettingDefinition& setting, SettingView& view,
pugi::xml_node layout );
UICheckBox* createBoolControl( PanelState& state, SettingDefinition& setting,
SettingView& view );
void materializeCategory( PanelState& state, const std::string& category );
void materializeVisibleSettings( PanelState& state, const String& query );
void setCategoryEnabled( PanelState& state, const std::string& category, bool enabled,
const std::string& excludedSetting = {} );
void filter( PanelState& state );