diff --git a/bin/assets/i18n/de.xml b/bin/assets/i18n/de.xml
index 04ba49cad..02ddbf361 100644
--- a/bin/assets/i18n/de.xml
+++ b/bin/assets/i18n/de.xml
@@ -307,6 +307,7 @@ Text selbst.
Unicode-Zeichen \, \t, \n, \r und ( ersetzen
Escape-Sequenzen
Exklusivmodus
+ Exklusivmodus aktivieren
Auszuführende Programmdatei
Verzeichnis in Konsole öffnen
In Konsole ausführen
diff --git a/bin/assets/i18n/en.xml b/bin/assets/i18n/en.xml
index 36be4506e..586295b0e 100644
--- a/bin/assets/i18n/en.xml
+++ b/bin/assets/i18n/en.xml
@@ -291,6 +291,7 @@ is hover a word that represents a color.
Replace \, \t, \n, \r and (Unicode characters
Escape Sequences
Exclusive Mode
+ Enable Exclusive Mode
Executable to Run
Open directory in terminal
Execute in terminal
@@ -886,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/fr.xml b/bin/assets/i18n/fr.xml
index b1abe9313..de1ce5a2d 100644
--- a/bin/assets/i18n/fr.xml
+++ b/bin/assets/i18n/fr.xml
@@ -290,6 +290,7 @@ texte lui-même.
Remplacer \, \t, \n, \r et (caractères Unicode)
Séquences d'échappement
Mode exclusif
+ Activer le mode exclusif
Exécutable à exécuter
Ouvrir le répertoire dans le terminal
Exécuter dans le terminal
diff --git a/bin/assets/i18n/zh.xml b/bin/assets/i18n/zh.xml
index d347ecc64..de3b2502c 100644
--- a/bin/assets/i18n/zh.xml
+++ b/bin/assets/i18n/zh.xml
@@ -223,6 +223,7 @@
,
和 (Unicode字符
Exclusive Mode
+ 启用独占模式
在终端打开文件夹
在终端执行
Expand All
@@ -680,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 283e3cc26..5ab9f023d 100644
--- a/include/eepp/ui.hpp
+++ b/include/eepp/ui.hpp
@@ -96,6 +96,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/include/eepp/ui/css/stylesheetselector.hpp b/include/eepp/ui/css/stylesheetselector.hpp
index 17443351a..8638a6817 100644
--- a/include/eepp/ui/css/stylesheetselector.hpp
+++ b/include/eepp/ui/css/stylesheetselector.hpp
@@ -27,7 +27,7 @@ class EE_API StyleSheetSelector {
bool hasPseudoClasses() const;
- std::vector getRelatedElements( UIWidget* element, bool applyPseudo = true ) const;
+ SmallVector getRelatedElements( UIWidget* element, bool applyPseudo = true ) const;
bool isStructurallyVolatile() const;
diff --git a/include/eepp/ui/databinding/uiproperty.hpp b/include/eepp/ui/databinding/uiproperty.hpp
index f03dd8e27..c5af9fd39 100644
--- a/include/eepp/ui/databinding/uiproperty.hpp
+++ b/include/eepp/ui/databinding/uiproperty.hpp
@@ -123,7 +123,7 @@ template class UIProperty {
}
/** @return All currently connected widgets, or an empty vector after expiration. */
- std::vector widgets() const {
+ SmallVector widgets() const {
auto state = mState.lock();
if ( !state || !state->property || !state->property->databind().isInitialized() )
return {};
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/include/eepp/ui/uinode.hpp b/include/eepp/ui/uinode.hpp
index ee3eef0a8..1cf64a17b 100644
--- a/include/eepp/ui/uinode.hpp
+++ b/include/eepp/ui/uinode.hpp
@@ -1516,8 +1516,14 @@ class EE_API UINode : public Node {
/** @brief Get a widget's computed absolute font size in pixels. */
Float getAbsoluteFontSize( const UIWidget* widget ) const;
+ /** Returns true if the node is currently being created, this state is not used by all node
+ * types.
+ */
bool isCreatingNode() const;
+ /** Forces a left mouse click event over the node */
+ void click();
+
protected:
Vector2f mDpPos;
Sizef mDpSize;
diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp
index 7ede8e369..47751f26c 100644
--- a/include/eepp/ui/uiscenenode.hpp
+++ b/include/eepp/ui/uiscenenode.hpp
@@ -864,7 +864,8 @@ class EE_API UISceneNode : public SceneNode {
* @param marker Marker for style association.
* @return Vector of root widgets created.
*/
- std::vector loadNode( pugi::xml_node node, Node* parent, const Uint32& marker = 0 );
+ SmallVector loadNode( pugi::xml_node node, Node* parent,
+ const Uint32& marker = 0 );
/** Sets the document / scene URI used to resolve paths of inner elements */
void setURI( const URI& uri );
diff --git a/include/eepp/ui/uiwidget.hpp b/include/eepp/ui/uiwidget.hpp
index aa4dc176d..fb9298510 100644
--- a/include/eepp/ui/uiwidget.hpp
+++ b/include/eepp/ui/uiwidget.hpp
@@ -26,6 +26,7 @@ class UITooltip;
class UIStyle;
class AccessibilityWidgetResolver;
struct AccessibilityProperties;
+class UIWidget;
struct MarginAuto {
static constexpr auto Left = ( 1 << 0 );
@@ -34,6 +35,8 @@ struct MarginAuto {
static constexpr auto Bottom = ( 1 << 3 );
};
+using WidgetQueryResult = SmallVector;
+
/**
* @brief Base class for all UI widgets in the eepp framework.
*
@@ -1153,7 +1156,7 @@ class EE_API UIWidget : public UINode {
* @param className The CSS class to search for.
* @return Vector of matching UIWidget pointers.
*/
- std::vector findAllByClass( const std::string& className );
+ WidgetQueryResult findAllByClass( const std::string& className );
/**
* @brief Finds all widgets by CSS tag.
@@ -1163,7 +1166,7 @@ class EE_API UIWidget : public UINode {
* @param tag The CSS tag to search for.
* @return Vector of matching UIWidget pointers.
*/
- std::vector findAllByTag( const std::string& tag );
+ WidgetQueryResult findAllByTag( const std::string& tag );
/**
* @brief Finds a widget by CSS class.
@@ -1225,7 +1228,7 @@ class EE_API UIWidget : public UINode {
* @param selector The CSS selector to use.
* @return Vector of all matching UIWidget pointers.
*/
- std::vector querySelectorAll( const CSS::StyleSheetSelector& selector );
+ WidgetQueryResult querySelectorAll( const CSS::StyleSheetSelector& selector );
/**
* @brief Queries all widgets using a CSS selector string.
@@ -1235,7 +1238,7 @@ class EE_API UIWidget : public UINode {
* @param selector The CSS selector string to use.
* @return Vector of all matching UIWidget pointers.
*/
- std::vector querySelectorAll( const std::string& selector );
+ WidgetQueryResult querySelectorAll( const std::string& selector );
/**
* @brief Gets a property value as a string.
diff --git a/include/eepp/window/clipboard.hpp b/include/eepp/window/clipboard.hpp
index 6302794bd..b9184a725 100644
--- a/include/eepp/window/clipboard.hpp
+++ b/include/eepp/window/clipboard.hpp
@@ -7,6 +7,10 @@ namespace EE { namespace Window {
class EE_API Clipboard {
public:
+ using Data = std::vector;
+ using DataCallback = std::function;
+ using CleanupCallback = std::function;
+
virtual ~Clipboard();
/** @return The Clipboard Text if available */
@@ -30,6 +34,29 @@ class EE_API Clipboard {
/** Set the current clipboard primary selection text */
virtual void setPrimarySelectionText( const std::string& text ) {}
+ /**
+ * Offer non-text clipboard data in one or more MIME types. The data callback is invoked lazily
+ * when the operating system requests a format, and with a null MIME type when the offer is
+ * cleared or replaced. The returned data must remain valid until the cleanup callback is
+ * invoked. Only implemented by the SDL3 backend.
+ */
+ virtual bool setData( DataCallback callback, CleanupCallback cleanup,
+ const std::vector& mimeTypes ) {
+ return false;
+ }
+
+ /** Clear all clipboard data. Only implemented by the SDL3 backend. */
+ virtual bool clearData() { return false; }
+
+ /** Get clipboard data for a MIME type. Only implemented by the SDL3 backend. */
+ virtual Data getData( const std::string& mimeType ) { return {}; }
+
+ /** @return The MIME types currently offered by the clipboard. SDL3 backend only. */
+ virtual std::vector getMimeTypes() { return {}; }
+
+ /** @return Whether clipboard data is available for a MIME type. SDL3 backend only. */
+ virtual bool hasData( const std::string& mimeType ) const { return false; }
+
protected:
friend class Window;
diff --git a/src/eepp/ui/css/stylesheetselector.cpp b/src/eepp/ui/css/stylesheetselector.cpp
index a673cd37a..1ee064166 100644
--- a/src/eepp/ui/css/stylesheetselector.cpp
+++ b/src/eepp/ui/css/stylesheetselector.cpp
@@ -237,10 +237,10 @@ bool StyleSheetSelector::selectComplex( UIWidget* element, const bool& applyPseu
return true;
}
-std::vector StyleSheetSelector::getRelatedElements( UIWidget* element,
- bool applyPseudo ) const {
- static std::vector EMPTY_ELEMENTS;
- std::vector elements;
+SmallVector StyleSheetSelector::getRelatedElements( UIWidget* element,
+ bool applyPseudo ) const {
+ static SmallVector EMPTY_ELEMENTS;
+ SmallVector elements;
if ( mSelectorRules.empty() )
return elements;
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/eepp/ui/uinode.cpp b/src/eepp/ui/uinode.cpp
index 35b379ea4..894d5d0dd 100644
--- a/src/eepp/ui/uinode.cpp
+++ b/src/eepp/ui/uinode.cpp
@@ -1905,4 +1905,10 @@ bool UINode::isCreatingNode() const {
return mFlags & UI_CREATING_NODE;
}
+void UINode::click() {
+ Vector2f pos;
+ nodeToWorld( pos );
+ getEventDispatcher()->sendMouseClick( this, pos.ceil().asInt(), EE_BUTTON_LMASK );
+}
+
}} // namespace EE::UI
diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp
index 4c758d32e..8cf82b98f 100644
--- a/src/eepp/ui/uiscenenode.cpp
+++ b/src/eepp/ui/uiscenenode.cpp
@@ -548,12 +548,12 @@ bool UISceneNode::windowExists( UIWindow* win ) {
return mWindowsList.end() != std::find( mWindowsList.begin(), mWindowsList.end(), win );
}
-std::vector UISceneNode::loadNode( pugi::xml_node node, Node* parent,
+SmallVector UISceneNode::loadNode( pugi::xml_node node, Node* parent,
const Uint32& marker ) {
Uint32 oldMarker = mCurrentMarker;
mCurrentMarker = marker;
- std::vector rootWidgets;
+ SmallVector rootWidgets;
if ( NULL == parent )
parent = this;
@@ -648,7 +648,7 @@ UIWidget* UISceneNode::loadLayoutNodes( pugi::xml_node node, Node* parent, const
std::string id( node.attribute( "id" ).as_string() );
mIsLoading = true;
Clock innerClock;
- std::vector widgets = loadNode( node, parent, marker );
+ SmallVector widgets = loadNode( node, parent, marker );
if ( mVerbose ) {
std::sort(
diff --git a/src/eepp/ui/uistyle.cpp b/src/eepp/ui/uistyle.cpp
index 5d253e63c..1b3a31bee 100644
--- a/src/eepp/ui/uistyle.cpp
+++ b/src/eepp/ui/uistyle.cpp
@@ -581,8 +581,7 @@ void UIStyle::subscribeNonCacheableStyles() {
return;
for ( auto& style : mGlobalDefinition->getStyles() ) {
if ( !style->getSelector().isCacheable() ) {
- std::vector elements =
- style->getSelector().getRelatedElements( mWidget, false );
+ auto elements = style->getSelector().getRelatedElements( mWidget, false );
if ( !elements.empty() ) {
for ( auto& element : elements ) {
diff --git a/src/eepp/ui/uiwidget.cpp b/src/eepp/ui/uiwidget.cpp
index 27e64627c..a2091cb65 100644
--- a/src/eepp/ui/uiwidget.cpp
+++ b/src/eepp/ui/uiwidget.cpp
@@ -1705,8 +1705,8 @@ const Uint32& UIWidget::getStylePreviousState() const {
return NULL != mStyle ? mStyle->getPreviousState() : mState;
}
-std::vector UIWidget::findAllByClass( const std::string& className ) {
- std::vector widgets;
+WidgetQueryResult UIWidget::findAllByClass( const std::string& className ) {
+ WidgetQueryResult widgets;
if ( !isClosing() && hasClass( className ) && !inClosingTree() ) {
widgets.push_back( this );
@@ -1716,8 +1716,7 @@ std::vector UIWidget::findAllByClass( const std::string& className )
while ( NULL != child ) {
if ( child->isWidget() ) {
- std::vector foundWidgets =
- child->asType()->findAllByClass( className );
+ WidgetQueryResult foundWidgets = child->asType()->findAllByClass( className );
if ( !foundWidgets.empty() )
widgets.insert( widgets.end(), foundWidgets.begin(), foundWidgets.end() );
@@ -1729,8 +1728,8 @@ std::vector UIWidget::findAllByClass( const std::string& className )
return widgets;
}
-std::vector UIWidget::findAllByTag( const std::string& tag ) {
- std::vector widgets;
+WidgetQueryResult UIWidget::findAllByTag( const std::string& tag ) {
+ WidgetQueryResult widgets;
if ( !isClosing() && getElementTag() == tag && !inClosingTree() ) {
widgets.push_back( this );
@@ -1740,7 +1739,7 @@ std::vector UIWidget::findAllByTag( const std::string& tag ) {
while ( NULL != child ) {
if ( child->isWidget() ) {
- std::vector foundWidgets = child->asType()->findAllByTag( tag );
+ WidgetQueryResult foundWidgets = child->asType()->findAllByTag( tag );
if ( !foundWidgets.empty() )
widgets.insert( widgets.end(), foundWidgets.begin(), foundWidgets.end() );
@@ -1815,8 +1814,8 @@ UIWidget* UIWidget::querySelector( const CSS::StyleSheetSelector& selector ) {
return NULL;
}
-std::vector UIWidget::querySelectorAll( const CSS::StyleSheetSelector& selector ) {
- std::vector widgets;
+WidgetQueryResult UIWidget::querySelectorAll( const CSS::StyleSheetSelector& selector ) {
+ WidgetQueryResult widgets;
if ( !isClosing() && !inClosingTree() && selector.select( this ) ) {
widgets.push_back( this );
@@ -1826,7 +1825,7 @@ std::vector UIWidget::querySelectorAll( const CSS::StyleSheetSelector
while ( NULL != child ) {
if ( child->isWidget() ) {
- std::vector foundWidgets =
+ WidgetQueryResult foundWidgets =
child->asType()->querySelectorAll( selector );
if ( !foundWidgets.empty() )
@@ -1862,7 +1861,7 @@ UIWidget* UIWidget::querySelector( const std::string& selector ) {
return querySelector( CSS::StyleSheetSelector( selector ) );
}
-std::vector UIWidget::querySelectorAll( const std::string& selector ) {
+WidgetQueryResult UIWidget::querySelectorAll( const std::string& selector ) {
return querySelectorAll( CSS::StyleSheetSelector( selector ) );
}
diff --git a/src/eepp/window/backend/SDL3/clipboardsdl3.cpp b/src/eepp/window/backend/SDL3/clipboardsdl3.cpp
index e93d996d6..93086084e 100644
--- a/src/eepp/window/backend/SDL3/clipboardsdl3.cpp
+++ b/src/eepp/window/backend/SDL3/clipboardsdl3.cpp
@@ -2,10 +2,32 @@
#include
#include
+#include
+
#ifdef EE_BACKEND_SDL3
namespace EE { namespace Window { namespace Backend { namespace SDL3 {
+namespace {
+
+struct ClipboardDataContext {
+ Clipboard::DataCallback callback;
+ Clipboard::CleanupCallback cleanup;
+};
+
+const void* SDLCALL clipboardDataCallback( void* userdata, const char* mimeType, size_t* size ) {
+ auto context = static_cast( userdata );
+ return context->callback( mimeType, size );
+}
+
+void SDLCALL clipboardCleanupCallback( void* userdata ) {
+ std::unique_ptr context( static_cast( userdata ) );
+ if ( context->cleanup )
+ context->cleanup();
+}
+
+} // namespace
+
ClipboardSDL::ClipboardSDL( EE::Window::Window* window ) : Clipboard( window ) {}
ClipboardSDL::~ClipboardSDL() {}
@@ -40,6 +62,60 @@ String ClipboardSDL::getWideText() {
return String::fromUtf8( getText() );
}
+bool ClipboardSDL::setData( DataCallback callback, CleanupCallback cleanup,
+ const std::vector& mimeTypes ) {
+ if ( !callback || mimeTypes.empty() )
+ return false;
+
+ std::vector mimeTypePointers;
+ mimeTypePointers.reserve( mimeTypes.size() );
+ for ( const auto& mimeType : mimeTypes )
+ mimeTypePointers.emplace_back( mimeType.c_str() );
+
+ auto context = std::make_unique(
+ ClipboardDataContext{ std::move( callback ), std::move( cleanup ) } );
+ if ( !SDL_SetClipboardData( clipboardDataCallback, clipboardCleanupCallback, context.get(),
+ mimeTypePointers.data(), mimeTypePointers.size() ) )
+ return false;
+
+ context.release();
+ return true;
+}
+
+bool ClipboardSDL::clearData() {
+ return SDL_ClearClipboardData();
+}
+
+Clipboard::Data ClipboardSDL::getData( const std::string& mimeType ) {
+ size_t size = 0;
+ void* data = SDL_GetClipboardData( mimeType.c_str(), &size );
+ if ( !data )
+ return {};
+
+ const auto bytes = static_cast( data );
+ Data result( bytes, bytes + size );
+ SDL_free( data );
+ return result;
+}
+
+std::vector ClipboardSDL::getMimeTypes() {
+ size_t count = 0;
+ char** mimeTypes = SDL_GetClipboardMimeTypes( &count );
+ if ( !mimeTypes )
+ return {};
+
+ std::vector result;
+ result.reserve( count );
+ for ( size_t i = 0; i < count; ++i )
+ result.emplace_back( mimeTypes[i] );
+ SDL_free( mimeTypes );
+ return result;
+}
+
+bool ClipboardSDL::hasData( const std::string& mimeType ) const {
+ return SDL_HasClipboardData( mimeType.c_str() );
+}
+
}}}} // namespace EE::Window::Backend::SDL3
#endif
diff --git a/src/eepp/window/backend/SDL3/clipboardsdl3.hpp b/src/eepp/window/backend/SDL3/clipboardsdl3.hpp
index b2f3cbedf..a38cb8f7c 100644
--- a/src/eepp/window/backend/SDL3/clipboardsdl3.hpp
+++ b/src/eepp/window/backend/SDL3/clipboardsdl3.hpp
@@ -27,6 +27,17 @@ class EE_API ClipboardSDL : public Clipboard {
void setPrimarySelectionText( const std::string& text );
+ bool setData( DataCallback callback, CleanupCallback cleanup,
+ const std::vector& mimeTypes );
+
+ bool clearData();
+
+ Data getData( const std::string& mimeType );
+
+ std::vector getMimeTypes();
+
+ bool hasData( const std::string& mimeType ) const;
+
protected:
friend class WindowSDL;
diff --git a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp
index 26f93222f..40bc5fc7e 100644
--- a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp
+++ b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp
@@ -1876,7 +1876,10 @@ void TerminalDisplay::onSizeChange() {
mSession->resize( gridSize.getWidth(), gridSize.getHeight(),
gridSize.getWidth() * cellSize.getWidth(),
gridSize.getHeight() * cellSize.getHeight() );
- mDirtyLines.resize( gridSize.getHeight(), 1 );
+ // Rendering must continue using the currently published snapshot until the worker
+ // publishes the resized grid. consumeSnapshot() updates mRows and mDirtyLines together;
+ // shrinking the dirty vector here leaves drawGrid() indexing it with the old mRows.
+ mDirtyLines.resize( std::max( mRows, gridSize.getHeight() ), true );
}
if ( mFrameBuffer && ( mFrameBuffer->getWidth() < mSize.getWidth() ||
diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp
index 3f58f51e5..76abdbce6 100644
--- a/src/tests/unit_tests/uihtml_tests.cpp
+++ b/src/tests/unit_tests/uihtml_tests.cpp
@@ -5837,7 +5837,7 @@ UTEST( UIHTML, RonStonerDeferredImagesUpdateDocumentHeight ) {
ASSERT_TRUE( documentScene != nullptr );
UIWidget* body = nullptr;
- std::vector images;
+ WidgetQueryResult images;
for ( int i = 0; i < 300; ++i ) {
pump();
body = documentScene->getRoot()->findByType( UI_TYPE_HTML_BODY )->asType();
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/ecode.cpp b/src/tools/ecode/ecode.cpp
index d9fd15896..8a79ddb2e 100644
--- a/src/tools/ecode/ecode.cpp
+++ b/src/tools/ecode/ecode.cpp
@@ -556,6 +556,8 @@ void App::maximizeTabWidget() {
UIWindow::StyleConfig winCfg;
winCfg.WinFlags = UI_WIN_SHADOW | UI_WIN_MODAL | UI_WIN_EPHEMERAL | UI_WIN_NO_DECORATION;
UIWindow* win = UIWindow::NewOpt( UIWindow::SIMPLE_LAYOUT, winCfg );
+ mMaximizedTabWidgetWindow = win;
+ mMaximizedTabWidget = curTabWidget;
win->setPixelsSize( getUISceneNode()->getPixelsSize() - PixelDensity::dpToPx( 64 ) );
win->setId( "detached_tab_widget_win" );
win->addClass( "tab_widget_cont" );
@@ -587,6 +589,7 @@ void App::maximizeTabWidget() {
bool wasFirstSplit = tabWidgetParent->isType( UI_TYPE_SPLITTER ) &&
tabWidgetParent->asType()->getFirstWidget() == curTabWidget;
auto nodeLink = UINodeLink::NewLink( curTabWidget );
+ mMaximizedTabWidgetLink = nodeLink;
if ( wasFirstSplit )
nodeLink->setClass( "was_first_split" );
curTabWidget->setParent( win );
@@ -626,27 +629,24 @@ void App::maximizeTabWidget() {
void App::restoreMaximizedTabWidget() {
if ( !App::instance() || !SceneManager::isActive() )
return;
- auto sceneNode = appInstance->getUISceneNode();
- if ( !sceneNode )
- return;
- auto nodeLink = sceneNode->getRoot()->find( "nodelink_tab_widget" );
- if ( !nodeLink )
- return;
- auto splitterParent = nodeLink->getParent();
- nodeLink->setParent( sceneNode );
- auto curTabWidget = sceneNode->getRoot()->find( "detached_tab_widget" );
- if ( !curTabWidget )
+ if ( !mMaximizedTabWidgetLink || !mMaximizedTabWidget )
return;
+ auto* nodeLink = mMaximizedTabWidgetLink;
+ auto* curTabWidget = mMaximizedTabWidget;
+ auto* win = mMaximizedTabWidgetWindow;
+ auto* splitterParent = nodeLink->getParent();
+ nodeLink->setParent( mUISceneNode );
curTabWidget->setParent( splitterParent );
curTabWidget->setAnchors( 0 );
curTabWidget->setId( "" );
- if ( nodeLink->asType()->hasClass( "was_first_split" ) &&
- splitterParent->isType( UI_TYPE_SPLITTER ) ) {
+ if ( nodeLink->hasClass( "was_first_split" ) && splitterParent->isType( UI_TYPE_SPLITTER ) ) {
splitterParent->asType()->swap();
}
nodeLink->close();
- auto win = mUISceneNode->find( "detached_tab_widget_win" );
- if ( win )
+ mMaximizedTabWidgetLink = nullptr;
+ mMaximizedTabWidget = nullptr;
+ mMaximizedTabWidgetWindow = nullptr;
+ if ( win && !win->isClosing() )
win->close();
}
@@ -1734,6 +1734,16 @@ void App::onTabCreated( UITab* tab, UIWidget* ) {
if ( tab->getOwnedWidget()->isType( UI_TYPE_CODEEDITOR ) ||
tab->getOwnedWidget()->isType( UI_TYPE_TERMINAL ) ) {
+ if ( tab->getOwnedWidget()->isType( UI_TYPE_TERMINAL ) ) {
+ menu->addSeparator();
+ auto* terminal = tab->getOwnedWidget()->asType();
+ menu->addCheckBox( i18n( "enable_exclusive_mode", "Enable Exclusive Mode" ),
+ terminal->getExclusiveMode(),
+ getKeybind( UITerminal::getExclusiveModeToggleCommandName() ) )
+ ->setId( UITerminal::getExclusiveModeToggleCommandName() );
+ menuAdd( "rename_session", "Rename Session", "", "terminal-rename" );
+ }
+
menu->addSeparator();
bool enabled = tab->getTabWidget()->getTabCount() > 1;
@@ -5073,6 +5083,11 @@ void App::init( InitParameters& params ) {
std::function onMsgBoxCloseCb ) -> bool {
if ( widget == nullptr || widget->getData() == 0 )
return true;
+ if ( auto* detachedTabWidget =
+ mUISceneNode->getRoot()->find( "detached_tab_widget" );
+ detachedTabWidget && detachedTabWidget->inParentTreeOf( widget ) ) {
+ restoreMaximizedTabWidget();
+ }
if ( widget->isType( UI_TYPE_CODEEDITOR ) ) {
return mSplitter->tryCodeEditorClose( widget->asType