diff --git a/.agent/SOUL.md b/.agent/SOUL.md
index 86acae583..e93d80a97 100644
--- a/.agent/SOUL.md
+++ b/.agent/SOUL.md
@@ -8,8 +8,19 @@ Your name is Negen (from negentropy: the process of creating order out of chaos)
1. **Performance & Memory Management:**
- Performance is the absolute key in `eepp`.
- Favor stack-allocated memory over heap allocations whenever possible.
+ - Prefer eepp's internal container layer when it provides the required semantics. Check
+ `include/eepp/core/containers.hpp`, `small_vector.hpp`, `lrucache.hpp`, and related core
+ containers before introducing standard-library or third-party containers directly.
+ - Also consider `include/eepp/core/small_function.hpp` for frequently stored callbacks with
+ known, bounded capture sizes. It is not a general replacement for `std::function`: use it only
+ when its inline-capacity, callable semantics, and object-size tradeoff fit the concrete use.
- Any heap allocation must be heavily justified.
- Exercise reason: maximize stack use for speed, but actively calculate boundaries to prevent stack-overflows.
+ - Review the memory layout of every new or materially changed struct and class. Order members
+ and select appropriately sized enum/integer storage to minimize alignment padding, and verify
+ meaningful changes with compiler layout data or `sizeof` instead of guessing. Do not use packed
+ layouts or otherwise force misaligned access, and preserve public ABI unless the change is
+ explicitly authorized.
- Before finalizing C++ changes, perform an explicit allocation audit:
- Review every heap allocation, string copy, container insertion, `std::function`, lambda capture, and async handoff introduced or touched by the change.
- Prefer move captures for owned temporary strings, buffers, vectors, and other heap-backed objects passed into lambdas.
diff --git a/.agent/rules/project-introduction.md b/.agent/rules/project-introduction.md
index 184c0d1dc..3adf631e5 100644
--- a/.agent/rules/project-introduction.md
+++ b/.agent/rules/project-introduction.md
@@ -17,9 +17,24 @@ When working on this project, rely on the following resources to understand exis
* **Implementation Examples:** A wide variety of examples showing how to use the library are located in `src/examples/`.
* **General Context:** The `README.md` at the root directory contains deeper project details.
+## Localization
+
+The locale catalogs under `bin/assets/i18n/` belong to **ecode**; the other applications are not
+currently localized. Whenever an ecode feature introduces or changes a user-facing i18n key, add or
+update that key in every catalog in this directory. Do not rely only on the fallback string embedded
+in the source. Keep all locale files structurally valid and verify that every supported catalog
+contains the new or renamed key.
+
## C++ Virtual Method Style
Follow the convention already used by the class being edited. In particular, when a class declares
virtual methods without the `override` specifier, do not introduce `override` on new methods in that
class. Mixing the styles can enable Clang's inconsistent-missing-override warnings for the existing
declarations. A class-wide conversion is a separate change and must update all applicable methods
together.
+
+## Namespace Style
+
+Follow eepp's established namespace style: prefer the appropriate `using namespace EE::...`
+declarations and unqualified eepp type names, such as `UISplitter`, over repeatedly spelling fully
+qualified names such as `EE::UI::UISplitter`. Keep explicit qualification only where it is required
+to resolve ambiguity or avoid importing an unusually broad namespace into an unsuitable scope.
diff --git a/.agent/rules/unit-tests.md b/.agent/rules/unit-tests.md
index d3c8a7266..6c07be1ff 100644
--- a/.agent/rules/unit-tests.md
+++ b/.agent/rules/unit-tests.md
@@ -5,20 +5,22 @@ This project relies on a comprehensive suite of unit tests to prevent regression
## Running Tests
The test binary manages its own current working directory, so you can execute it from anywhere.
+* **Prefer the release test binary during normal development:**
+ When AddressSanitizer or other debug-only diagnostics are not required, build and run `bin/unit_tests/eepp-unit_tests`. The optimized release suite is substantially faster and should be the default for iterative testing. Use `bin/unit_tests/eepp-unit_tests-debug` when investigating memory safety, assertions, or other behavior that specifically requires the debug configuration.
* **Default Execution for Agents on Linux & FreeBSD:**
Always run unit tests through the project wrapper unless the user explicitly asks for a different harness:
- `projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug`
+ `projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests`
* **Why the wrapper is required:**
Tests open ~400 individual windows. The wrapper runs them in an isolated framebuffer, enables race-safe automatic display selection for concurrent agent test runs, sets the default screen to `1280x1024x24`, and injects `ASAN_OPTIONS=detect_leaks=0` automatically.
* **Do not skip the wrapper for filtered tests:**
A focused test still needs the same wrapper:
- `projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="FontRendering.*Offset*"`
+ `projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests --filter="FontRendering.*Offset*"`
* **Fallback only when the wrapper itself fails:**
If `projects/scripts/xvfb-run-eepp` fails before launching the test binary, report that wrapper failure and then use this fallback to keep verification moving:
- `ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug`
+ `xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests`
Do not use plain `xvfb-run` as the first attempt for GUI/unit tests.
* **Direct Execution (Only for non-window tests or explicit user requests):**
- `bin/unit_tests/eepp-unit_tests-debug`
+ `bin/unit_tests/eepp-unit_tests`
* **Filtering Tests:**
Use the `--filter` parameter to run specific tests (supports glob patterns).
Keep the wrapper in front of the binary unless the test is known not to create windows.
diff --git a/bin/assets/i18n/de.xml b/bin/assets/i18n/de.xml
index 1ea608b9d..21f09c73a 100644
--- a/bin/assets/i18n/de.xml
+++ b/bin/assets/i18n/de.xml
@@ -890,6 +890,8 @@ in der Baumansicht und in Öffen-/Schließdialogen aktivieren.
Eine Datei auswählen
Einen Ordner auswählen
Tabelle
+ Spalten an Ansicht anpassen
+ Freie Spaltenbreiten
Kopieren
Link öffnen
Einfügen
diff --git a/bin/assets/i18n/en.xml b/bin/assets/i18n/en.xml
index 8f6462b23..58d69c73f 100644
--- a/bin/assets/i18n/en.xml
+++ b/bin/assets/i18n/en.xml
@@ -874,6 +874,8 @@ the directory tree and in file dialogs to open a folder or file.
Select a file
Select a folder
Table
+ Fit Columns to View
+ Free Column Widths
Copy
Open Link
Paste
diff --git a/bin/assets/i18n/fr.xml b/bin/assets/i18n/fr.xml
index 6b5233662..dfb659e9f 100644
--- a/bin/assets/i18n/fr.xml
+++ b/bin/assets/i18n/fr.xml
@@ -865,6 +865,8 @@ dans l'arborescence de répertoires ainsi que dans les boites de dialogues de s
Sélectionner un fichier
Sélectionner un dossier
Table
+ Ajuster les colonnes à la vue
+ Largeurs de colonnes libres
Copier
Ouvrir un lien
Coller
diff --git a/bin/assets/i18n/zh.xml b/bin/assets/i18n/zh.xml
index 01ab61456..003b4a15f 100644
--- a/bin/assets/i18n/zh.xml
+++ b/bin/assets/i18n/zh.xml
@@ -663,6 +663,8 @@ file in the directory tree.
选择文件
选择文件夹
Table
+ 列宽适应视图
+ 自由调整列宽
复制
打开链接
粘贴
diff --git a/docs/articles/cssspecification.md b/docs/articles/cssspecification.md
index 6e626869b..87a51aa64 100644
--- a/docs/articles/cssspecification.md
+++ b/docs/articles/cssspecification.md
@@ -534,6 +534,30 @@ Sets the width of the child elements of a grid layout.
---
+### column-width-mode
+
+Selects how table and tree view column widths are represented and resized.
+
+* Applicable to: EE::UI::UIAbstractTableView (TableView, TreeView)
+* Data Type: [string-list](#string-list-data-type)
+* Value List:
+ * `pixels`: Column widths are stored as fixed pixel lengths and can extend beyond the available content width.
+ * `percentage`: Visible column widths are stored as normalized percentages of the available content width. Resizing a column adjusts an adjacent visible column to preserve the total.
+* Default value: `pixels`
+
+---
+
+### column-width-mode-menu
+
+Enables a context menu on table and tree view column headers for switching between pixel and
+percentage column width modes.
+
+* Applicable to: EE::UI::UIAbstractTableView (TableView, TreeView)
+* Data Type: [boolean](#boolean-data-type)
+* Default value: `false`
+
+---
+
### cursor
Read [cursor](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor) documentation.
@@ -1934,8 +1958,8 @@ Sets the [skin](#skin) tint color.
### splitter-always-show
-Sets if the splitter divisor is always visible. If false it will be shown only if two views are
-attached to the splitter.
+Sets whether the splitter divisor remains visible whenever two views are attached. This property
+overrides `splitter-hide-on-edge`.
* Applicable to: EE::UI::UISplitter (Splitter)
* Data Type: [boolean](#boolean-data-type)
@@ -1943,6 +1967,17 @@ attached to the splitter.
---
+### splitter-hide-on-edge
+
+Hides the splitter divisor when its percentage partition is exactly `0%` or `100%`, allowing one
+view to consume the complete splitter. This has no effect when `splitter-always-show` is `true`.
+
+* Applicable to: EE::UI::UISplitter (Splitter)
+* Data Type: [boolean](#boolean-data-type)
+* Default value: `false`
+
+---
+
### splitter-partition
Sets the space occupied by the first view contained by the splitter.
diff --git a/include/eepp/ui/abstract/uiabstracttableview.hpp b/include/eepp/ui/abstract/uiabstracttableview.hpp
index 59bca6edb..c8ccfeec8 100644
--- a/include/eepp/ui/abstract/uiabstracttableview.hpp
+++ b/include/eepp/ui/abstract/uiabstracttableview.hpp
@@ -6,6 +6,7 @@
#include
#include
#include
+#include
#include
using namespace EE::Math;
@@ -20,6 +21,8 @@ namespace EE { namespace UI { namespace Abstract {
class EE_API UIAbstractTableView : public UIAbstractView {
public:
+ enum class ColumnWidthMode : Uint8 { Pixels, Percentage };
+
enum TableFlags : Uint32 {
TableFlagNone = 0,
TableFlagHeaders = ( 1 << 0 ),
@@ -85,6 +88,30 @@ class EE_API UIAbstractTableView : public UIAbstractView {
const Float& getColumnWidth( const size_t& colIndex ) const;
+ Float getColumnWidthPercentage( const size_t& colIndex ) const;
+
+ std::vector getColumnsWidthPercentage() const;
+
+ ColumnWidthMode getColumnWidthMode() const;
+
+ void setColumnWidthMode( ColumnWidthMode mode, bool convertCurrentWidths = true );
+
+ bool isColumnWidthModeMenuEnabled() const;
+
+ void setColumnWidthModeMenuEnabled( bool enabled );
+
+ void setColumnWidthPercentage( const size_t& colIndex, Float percentage );
+
+ void setColumnsWidthPercentage( const std::vector& percentages );
+
+ /** Serializes the column width mode and widths. Pixel widths are stored as dp. */
+ nlohmann::json serializeColumnWidths() const;
+
+ /** Restores column widths serialized by serializeColumnWidths(). Legacy percentage arrays are
+ * also accepted. Percentage values may be partial or contain surplus entries; pixel widths must
+ * match the model. Returns false if the data is invalid or incompatible with the model. */
+ bool unserializeColumnWidths( const nlohmann::json& widths );
+
virtual Float getMaxColumnContentWidth( const size_t& colIndex, bool bestGuess = false );
bool getAutoExpandOnSingleColumn() const;
@@ -180,6 +207,7 @@ class EE_API UIAbstractTableView : public UIAbstractView {
Float minHeight{ 0 };
Float maxWidth{ 0 };
Float width{ 0 };
+ Float percentage{ 0 };
bool visible{ true };
bool manuallySet{ false };
UIPushButton* widget{ nullptr };
@@ -210,6 +238,10 @@ class EE_API UIAbstractTableView : public UIAbstractView {
std::function mSetupCellCb;
Float mRowHeaderWidth{ 0 };
Uint32 mTableFlags{ UITABLE_DEFAULT_FLAGS };
+ ColumnWidthMode mColumnWidthMode{ ColumnWidthMode::Pixels };
+ bool mColumnWidthModeMenuEnabled{ false };
+ bool mUpdatingColumnsForScrollbars{ false };
+ std::string mPendingSerializedColumnWidths;
virtual ~UIAbstractTableView();
@@ -231,6 +263,8 @@ class EE_API UIAbstractTableView : public UIAbstractView {
virtual void updateColumnsWidth();
+ void restorePendingColumnWidths();
+
virtual Uint32 onFocus( NodeFocusReason reason );
virtual Uint32 onFocusLoss();
@@ -252,6 +286,8 @@ class EE_API UIAbstractTableView : public UIAbstractView {
virtual void onScrollChange();
+ virtual void onContentSizeChange();
+
virtual void onRowCreated( UITableRow* row );
virtual void onSortColumn( const size_t& colIndex );
@@ -270,6 +306,10 @@ class EE_API UIAbstractTableView : public UIAbstractView {
void resetColumnData();
+ void updatePercentageColumnWidths();
+
+ int adjacentVisibleColumn( size_t column ) const;
+
void buildRowHeader();
void updateRowHeader( int realRowIndex, const ModelIndex& index, Float yOffset );
diff --git a/include/eepp/ui/css/propertyids.hpp b/include/eepp/ui/css/propertyids.hpp
index b8d02415e..6ff243953 100644
--- a/include/eepp/ui/css/propertyids.hpp
+++ b/include/eepp/ui/css/propertyids.hpp
@@ -232,6 +232,7 @@ enum class PropertyId : Uint16 {
TabAllowSwitchTabsInEmptySpaces,
SplitterPartition,
SplitterAlwaysShow,
+ SplitterHideOnEdge,
DroppableHoveringColor,
TextAsFallback,
SelectOnClick,
@@ -304,6 +305,8 @@ enum class PropertyId : Uint16 {
IconSize,
SortIconSize,
MainColumn,
+ ColumnWidthMode,
+ ColumnWidthModeMenu,
RowHeaderWidth,
TableFlags,
IndentWidth,
diff --git a/include/eepp/ui/tools/uitabwidgetsplitter.hpp b/include/eepp/ui/tools/uitabwidgetsplitter.hpp
index 3219e97fd..7f9185bff 100644
--- a/include/eepp/ui/tools/uitabwidgetsplitter.hpp
+++ b/include/eepp/ui/tools/uitabwidgetsplitter.hpp
@@ -1,6 +1,8 @@
#ifndef EE_UI_TOOLS_UITABWIDGETSPLITTER_HPP
#define EE_UI_TOOLS_UITABWIDGETSPLITTER_HPP
+#include
+#include
#include
#include
#include
@@ -130,6 +132,8 @@ class EE_API UITabWidgetSplitter {
void setOnTabWidgetCreateCb( std::function cb );
+ void setOnTabWidgetCloseCb( std::function cb );
+
void closeSplitter( UISplitter* splitter );
void addRemainingTabWidgets( Node* widget );
@@ -254,10 +258,13 @@ class EE_API UITabWidgetSplitter {
Float mVisualSplitEdgePercent{ 0.1 };
Mutex mTabWidgetMutex;
std::function mOnTabWidgetCreateCb;
+ std::function mOnTabWidgetCloseCb;
std::function mCanCreateSplitFn;
TabTryCloseCallback mTabTryCloseCb;
mutable Mutex mWidgetTypesMutex;
- std::unordered_map mWidgetTypes;
+ UnorderedMap mWidgetTypes;
+ UnorderedMap mTabWidgetEventConnections;
+ UnorderedMap mWidgetEventConnections;
UITabWidgetSplitter( Client* client, UISceneNode* sceneNode );
diff --git a/include/eepp/ui/uisplitter.hpp b/include/eepp/ui/uisplitter.hpp
index 165db7655..f03c1bc7f 100644
--- a/include/eepp/ui/uisplitter.hpp
+++ b/include/eepp/ui/uisplitter.hpp
@@ -1,6 +1,7 @@
#ifndef EE_UI_UISPLITTER_HPP
#define EE_UI_UISPLITTER_HPP
+#include
#include
namespace EE { namespace UI {
@@ -23,6 +24,10 @@ class EE_API UISplitter : public UILayout {
void setAlwaysShowSplitter( bool alwaysShowSplitter );
+ const bool& hideSplitterOnEdge() const;
+
+ void setHideSplitterOnEdge( bool hideSplitterOnEdge );
+
const StyleSheetLength& getSplitPartition() const;
void setSplitPartition( const StyleSheetLength& divisionSplit );
@@ -49,10 +54,12 @@ class EE_API UISplitter : public UILayout {
protected:
UIOrientation mOrientation;
bool mAlwaysShowSplitter;
+ bool mHideSplitterOnEdge;
StyleSheetLength mSplitPartition;
UIWidget* mSplitter;
UIWidget* mFirstWidget;
UIWidget* mLastWidget;
+ Scene::EventConnectionList mEventConnections;
UISplitter();
@@ -63,6 +70,8 @@ class EE_API UISplitter : public UILayout {
void updateFromDrag();
void updateSplitterDragFlags();
+
+ bool shouldShowSplitter() const;
};
}} // namespace EE::UI
diff --git a/include/eepp/ui/uitableheadercolumn.hpp b/include/eepp/ui/uitableheadercolumn.hpp
index 112333cbd..53135ebe9 100644
--- a/include/eepp/ui/uitableheadercolumn.hpp
+++ b/include/eepp/ui/uitableheadercolumn.hpp
@@ -20,7 +20,7 @@ class EE_API UITableHeaderColumn : public UIPushButton {
protected:
UIAbstractTableView* mView;
size_t mColIndex;
- mutable UIImage* mImage{nullptr};
+ mutable UIImage* mImage{ nullptr };
Uint32 onCalculateDrag( const Vector2f& position, const Uint32& flags );
@@ -34,6 +34,8 @@ class EE_API UITableHeaderColumn : public UIPushButton {
Uint32 onMouseClick( const Vector2i& position, const Uint32& flags );
+ Uint32 onMouseUp( const Vector2i& position, const Uint32& flags );
+
Uint32 onMouseDoubleClick( const Vector2i& position, const Uint32& flags );
Uint32 onDragStop( const Vector2i& pos, const Uint32& flags );
diff --git a/include/eepp/ui/uitabwidget.hpp b/include/eepp/ui/uitabwidget.hpp
index aa9632bc4..7723afc23 100644
--- a/include/eepp/ui/uitabwidget.hpp
+++ b/include/eepp/ui/uitabwidget.hpp
@@ -2,6 +2,7 @@
#define EE_UI_UITABWIDGET_HPP
#include
+#include
#include
#include
#include
@@ -260,6 +261,7 @@ class EE_API UITabWidget : public UIWidget {
UIListView* mTabSwitcher{ nullptr };
TabJumpMode mTabJumpMode{ TabJumpMode::Linear };
std::function mAcceptsDropOfWidgetFn;
+ Scene::EventConnectionList mEventConnections;
UITabWidget();
diff --git a/src/eepp/ui/abstract/uiabstracttableview.cpp b/src/eepp/ui/abstract/uiabstracttableview.cpp
index 2679266c4..8f65466c3 100644
--- a/src/eepp/ui/abstract/uiabstracttableview.cpp
+++ b/src/eepp/ui/abstract/uiabstracttableview.cpp
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
namespace EE { namespace UI { namespace Abstract {
@@ -104,6 +105,166 @@ const Float& UIAbstractTableView::getColumnWidth( const size_t& colIndex ) const
return columnData( colIndex ).width;
}
+Float UIAbstractTableView::getColumnWidthPercentage( const size_t& colIndex ) const {
+ return columnData( colIndex ).percentage;
+}
+
+std::vector UIAbstractTableView::getColumnsWidthPercentage() const {
+ std::vector percentages;
+ size_t count = getModel() ? getModel()->columnCount() : mColumn.size();
+ percentages.reserve( count );
+ for ( size_t i = 0; i < count; ++i )
+ percentages.emplace_back( columnData( i ).percentage );
+ return percentages;
+}
+
+UIAbstractTableView::ColumnWidthMode UIAbstractTableView::getColumnWidthMode() const {
+ return mColumnWidthMode;
+}
+
+void UIAbstractTableView::setColumnWidthMode( ColumnWidthMode mode, bool convertCurrentWidths ) {
+ if ( mode == mColumnWidthMode )
+ return;
+ if ( mode == ColumnWidthMode::Percentage && getModel() ) {
+ setAutoColumnsWidth( false );
+ if ( convertCurrentWidths ) {
+ Float totalWidth = 0;
+ for ( size_t i = 0; i < getModel()->columnCount(); ++i )
+ if ( !isColumnHidden( i ) )
+ totalWidth += columnData( i ).width;
+ if ( totalWidth > 0 )
+ for ( size_t i = 0; i < getModel()->columnCount(); ++i )
+ if ( !isColumnHidden( i ) )
+ columnData( i ).percentage = columnData( i ).width / totalWidth * 100.f;
+ }
+ }
+ mColumnWidthMode = mode;
+ createOrUpdateColumns( false );
+}
+
+bool UIAbstractTableView::isColumnWidthModeMenuEnabled() const {
+ return mColumnWidthModeMenuEnabled;
+}
+
+void UIAbstractTableView::setColumnWidthModeMenuEnabled( bool enabled ) {
+ mColumnWidthModeMenuEnabled = enabled;
+}
+
+void UIAbstractTableView::setColumnWidthPercentage( const size_t& colIndex, Float percentage ) {
+ if ( mColumnWidthMode != ColumnWidthMode::Percentage )
+ setColumnWidthMode( ColumnWidthMode::Percentage );
+ if ( !getModel() || colIndex >= getModel()->columnCount() )
+ return;
+ auto& column = columnData( colIndex );
+ int adjacent = adjacentVisibleColumn( colIndex );
+ if ( adjacent >= 0 ) {
+ auto& sibling = columnData( adjacent );
+ Float combined = column.percentage + sibling.percentage;
+ column.percentage = eeclamp( percentage, 0.f, combined );
+ sibling.percentage = combined - column.percentage;
+ } else {
+ column.percentage = 100.f;
+ }
+ createOrUpdateColumns( false );
+}
+
+void UIAbstractTableView::setColumnsWidthPercentage( const std::vector& percentages ) {
+ const size_t columnCount = getModel() ? getModel()->columnCount() : percentages.size();
+ if ( mColumn.size() < columnCount )
+ mColumn.resize( columnCount );
+ const size_t suppliedColumnCount = eemin( percentages.size(), columnCount );
+ Float total = 0;
+ for ( size_t i = 0; i < suppliedColumnCount; ++i ) {
+ columnData( i ).percentage = eemax( 0.f, percentages[i] );
+ if ( !getModel() || !isColumnHidden( i ) )
+ total += columnData( i ).percentage;
+ }
+ size_t missingVisibleColumns = 0;
+ for ( size_t i = suppliedColumnCount; i < columnCount; ++i )
+ if ( !getModel() || !isColumnHidden( i ) )
+ ++missingVisibleColumns;
+ const Float missingPercentage =
+ missingVisibleColumns > 0 ? eemax( 0.f, 100.f - total ) / missingVisibleColumns : 0.f;
+ for ( size_t i = suppliedColumnCount; i < columnCount; ++i ) {
+ columnData( i ).percentage = !getModel() || !isColumnHidden( i ) ? missingPercentage : 0.f;
+ total += columnData( i ).percentage;
+ }
+ if ( total > 0 )
+ for ( size_t i = 0; i < columnCount; ++i )
+ if ( !getModel() || !isColumnHidden( i ) )
+ columnData( i ).percentage = columnData( i ).percentage / total * 100.f;
+ setAutoColumnsWidth( false );
+ mColumnWidthMode = ColumnWidthMode::Percentage;
+ if ( getModel() )
+ createOrUpdateColumns( false );
+}
+
+nlohmann::json UIAbstractTableView::serializeColumnWidths() const {
+ if ( !getModel() && !mPendingSerializedColumnWidths.empty() )
+ return nlohmann::json::parse( mPendingSerializedColumnWidths, nullptr, false, true );
+ nlohmann::json saved;
+ const bool percentage = mColumnWidthMode == ColumnWidthMode::Percentage;
+ saved["mode"] = percentage ? "percentage" : "pixels";
+ if ( percentage ) {
+ saved["widths"] = getColumnsWidthPercentage();
+ return saved;
+ }
+ std::vector widths;
+ if ( !getModel() )
+ return saved;
+ widths.reserve( getModel()->columnCount() );
+ for ( size_t i = 0; i < getModel()->columnCount(); ++i )
+ widths.emplace_back( PixelDensity::pxToDp( getColumnWidth( i ) ) );
+ saved["widths"] = std::move( widths );
+ return saved;
+}
+
+bool UIAbstractTableView::unserializeColumnWidths( const nlohmann::json& saved ) {
+ const nlohmann::json* widths = &saved;
+ ColumnWidthMode mode = ColumnWidthMode::Percentage;
+ if ( saved.is_object() ) {
+ if ( !saved.contains( "widths" ) || !saved["widths"].is_array() )
+ return false;
+ widths = &saved["widths"];
+ if ( saved.value( "mode", "percentage" ) == "pixels" )
+ mode = ColumnWidthMode::Pixels;
+ } else if ( !saved.is_array() ) {
+ return false;
+ }
+ std::vector values;
+ values.reserve( widths->size() );
+ for ( const auto& value : *widths ) {
+ if ( !value.is_number() )
+ return false;
+ values.emplace_back( value.get() );
+ }
+ if ( !getModel() ) {
+ mPendingSerializedColumnWidths = saved.dump();
+ return true;
+ }
+ mPendingSerializedColumnWidths.clear();
+ if ( mode == ColumnWidthMode::Percentage ) {
+ setColumnsWidthPercentage( values );
+ } else {
+ if ( values.size() != getModel()->columnCount() )
+ return false;
+ setColumnWidthMode( mode, false );
+ for ( size_t i = 0; i < values.size(); ++i )
+ setColumnWidth( i, PixelDensity::dpToPx( values[i] ) );
+ }
+ return true;
+}
+
+void UIAbstractTableView::restorePendingColumnWidths() {
+ if ( !getModel() || mPendingSerializedColumnWidths.empty() )
+ return;
+ std::string serialized( std::move( mPendingSerializedColumnWidths ) );
+ mPendingSerializedColumnWidths.clear();
+ auto widths = nlohmann::json::parse( serialized, nullptr, false, true );
+ if ( !widths.is_discarded() )
+ unserializeColumnWidths( widths );
+}
+
void UIAbstractTableView::selectAll() {
getSelection().clear();
for ( size_t itemIndex = 0; itemIndex < getItemCount(); ++itemIndex ) {
@@ -139,11 +300,13 @@ void UIAbstractTableView::onModelUpdate( unsigned flags ) {
[this] {
modelUpdate( mPendingUpdateFlags.exchange( 0 ) );
createOrUpdateColumns( true );
+ restorePendingColumnWidths();
},
Time::Zero, onModelUpdateTag );
} else {
UIAbstractView::onModelUpdate( flags );
createOrUpdateColumns( true );
+ restorePendingColumnWidths();
}
}
@@ -196,6 +359,9 @@ void UIAbstractTableView::createOrUpdateColumns( bool resetColumnData ) {
col.widget->setPixelsSize( col.width, getHeaderHeight() );
}
+ if ( mColumnWidthMode == ColumnWidthMode::Percentage )
+ updatePercentageColumnWidths();
+
if ( mAutoColumnsWidth && visibleColCount > 1 ) {
Float contentWidth = getContentSpaceWidth();
bool shouldVScrollBeVisible = shouldVerticalScrollBeVisible();
@@ -311,9 +477,92 @@ void UIAbstractTableView::onSizeChange() {
createOrUpdateColumns( false );
}
-void UIAbstractTableView::onColumnSizeChange( const size_t&, bool fromUserInteraction ) {
+void UIAbstractTableView::onColumnSizeChange( const size_t& colIndex, bool fromUserInteraction ) {
if ( fromUserInteraction && mAutoColumnsWidth )
mAutoColumnsWidth = false;
+ if ( fromUserInteraction && mColumnWidthMode == ColumnWidthMode::Percentage && getModel() ) {
+ int adjacent = adjacentVisibleColumn( colIndex );
+ Float contentWidth = getContentSpaceWidth();
+ if ( adjacent >= 0 && contentWidth > 0 ) {
+ auto& column = columnData( colIndex );
+ auto& sibling = columnData( adjacent );
+ Float combined = column.percentage + sibling.percentage;
+ Float minPercentage = column.minWidth / contentWidth * 100.f;
+ Float siblingMinPercentage = sibling.minWidth / contentWidth * 100.f;
+ column.percentage = eeclamp( column.width / contentWidth * 100.f, minPercentage,
+ combined - siblingMinPercentage );
+ sibling.percentage = combined - column.percentage;
+ updatePercentageColumnWidths();
+ updateHeaderSize();
+ }
+ }
+}
+
+void UIAbstractTableView::updatePercentageColumnWidths() {
+ if ( !getModel() )
+ return;
+ Float contentWidth = getContentSpaceWidth();
+ Float totalPercentage = 0;
+ int visibleColumns = 0;
+ for ( size_t i = 0; i < getModel()->columnCount(); ++i ) {
+ if ( !isColumnHidden( i ) ) {
+ totalPercentage += columnData( i ).percentage;
+ visibleColumns++;
+ }
+ }
+ if ( totalPercentage <= 0 && visibleColumns > 0 ) {
+ for ( size_t i = 0; i < getModel()->columnCount(); ++i )
+ if ( !isColumnHidden( i ) )
+ columnData( i ).percentage = 100.f / visibleColumns;
+ totalPercentage = 100.f;
+ }
+ contentWidth = eefloor( contentWidth );
+ Float cumulativePercentage = 0;
+ Float previousBoundary = 0;
+ Float assignedWidth = 0;
+ for ( size_t i = 0; i < getModel()->columnCount(); ++i ) {
+ if ( isColumnHidden( i ) )
+ continue;
+ auto& column = columnData( i );
+ cumulativePercentage += column.percentage;
+ Float boundary = eefloor( contentWidth * cumulativePercentage / totalPercentage + 0.5f );
+ Float width = boundary - previousBoundary;
+ Float minWidth = eeceil( column.minWidth );
+ width = column.maxWidth != 0
+ ? eeclamp( width, minWidth, eemax( minWidth, eefloor( column.maxWidth ) ) )
+ : eemax( width, minWidth );
+ column.setWidth( width, true );
+ assignedWidth += width;
+ previousBoundary = boundary;
+ }
+ Float overflow = assignedWidth - contentWidth;
+ for ( size_t i = getModel()->columnCount(); overflow > 0 && i > 0; --i ) {
+ if ( isColumnHidden( i - 1 ) )
+ continue;
+ auto& column = columnData( i - 1 );
+ Float shrink = eemin( overflow, column.width - eeceil( column.minWidth ) );
+ if ( shrink > 0 ) {
+ column.setWidth( column.width - shrink, true );
+ overflow -= shrink;
+ }
+ }
+ for ( size_t i = 0; i < getModel()->columnCount(); ++i ) {
+ auto& column = columnData( i );
+ if ( column.widget && !isColumnHidden( i ) )
+ column.widget->setPixelsSize( column.width, getHeaderHeight() );
+ }
+}
+
+int UIAbstractTableView::adjacentVisibleColumn( size_t column ) const {
+ if ( !getModel() )
+ return -1;
+ for ( size_t i = column + 1; i < getModel()->columnCount(); ++i )
+ if ( !isColumnHidden( i ) )
+ return i;
+ for ( size_t i = column; i > 0; --i )
+ if ( !isColumnHidden( i - 1 ) )
+ return i - 1;
+ return -1;
}
Float UIAbstractTableView::getMaxColumnContentWidth( const size_t&, bool ) {
@@ -322,6 +571,10 @@ Float UIAbstractTableView::getMaxColumnContentWidth( const size_t&, bool ) {
void UIAbstractTableView::onColumnResizeToContent( const size_t& colIndex ) {
columnData( colIndex ).setWidth( getMaxColumnContentWidth( colIndex, true ) );
+ if ( mColumnWidthMode == ColumnWidthMode::Percentage ) {
+ onColumnSizeChange( colIndex, true );
+ return;
+ }
createOrUpdateColumns( false );
}
@@ -582,6 +835,31 @@ void UIAbstractTableView::onScrollChange() {
mHeader->setPixelsPosition( mRowHeaderWidth + -mScrollOffset.x, 0 );
}
+void UIAbstractTableView::onContentSizeChange() {
+ if ( mUpdatingColumnsForScrollbars ) {
+ UIScrollableWidget::onContentSizeChange();
+ return;
+ }
+
+ bool verticalScrollWasVisible = mVScroll->isVisible();
+ UIScrollableWidget::onContentSizeChange();
+ const bool columnsDependOnContentWidth =
+ mColumnWidthMode == ColumnWidthMode::Percentage || mAutoColumnsWidth ||
+ ( mAutoExpandOnSingleColumn && visibleColumnCount() == 1 );
+ if ( !columnsDependOnContentWidth || verticalScrollWasVisible == mVScroll->isVisible() )
+ return;
+
+ mUpdatingColumnsForScrollbars = true;
+ for ( int iteration = 0; iteration < 2; ++iteration ) {
+ bool visibilityUsedForColumns = mVScroll->isVisible();
+ createOrUpdateColumns( false );
+ UIScrollableWidget::onContentSizeChange();
+ if ( visibilityUsedForColumns == mVScroll->isVisible() )
+ break;
+ }
+ mUpdatingColumnsForScrollbars = false;
+}
+
void UIAbstractTableView::bindNavigationClick( UIWidget* widget ) {
mWidgetsClickCbId[widget].push_back(
widget->on( Event::MouseDoubleClick, [this]( const Event* event ) {
@@ -1012,6 +1290,14 @@ bool UIAbstractTableView::applyProperty( const StyleSheetProperty& attribute ) {
case PropertyId::MainColumn:
setMainColumn( attribute.asInt() );
break;
+ case PropertyId::ColumnWidthMode:
+ setColumnWidthMode( String::iequals( attribute.getValue(), "percentage" )
+ ? ColumnWidthMode::Percentage
+ : ColumnWidthMode::Pixels );
+ break;
+ case PropertyId::ColumnWidthModeMenu:
+ setColumnWidthModeMenuEnabled( attribute.asBool() );
+ break;
case PropertyId::RowHeaderWidth:
setRowHeaderWidth(
lengthFromValue( attribute.getValue(), PropertyRelativeTarget::None ) );
@@ -1063,6 +1349,10 @@ std::string UIAbstractTableView::getPropertyString( const PropertyDefinition* pr
return String::fromFloat( (Float)getSortIconSize(), "px" );
case PropertyId::MainColumn:
return String::toString( (Int64)getMainColumn() );
+ case PropertyId::ColumnWidthMode:
+ return getColumnWidthMode() == ColumnWidthMode::Percentage ? "percentage" : "pixels";
+ case PropertyId::ColumnWidthModeMenu:
+ return isColumnWidthModeMenuEnabled() ? "true" : "false";
case PropertyId::RowHeaderWidth:
return String::fromFloat( getRowHeaderWidth(), "px" );
case PropertyId::TableFlags: {
@@ -1099,9 +1389,10 @@ std::string UIAbstractTableView::getPropertyString( const PropertyDefinition* pr
std::vector UIAbstractTableView::getPropertiesImplemented() const {
auto props = UIAbstractView::getPropertiesImplemented();
- props.insert( props.end(),
- { PropertyId::RowHeight, PropertyId::IconSize, PropertyId::SortIconSize,
- PropertyId::MainColumn, PropertyId::RowHeaderWidth, PropertyId::TableFlags } );
+ props.insert( props.end(), { PropertyId::RowHeight, PropertyId::IconSize,
+ PropertyId::SortIconSize, PropertyId::MainColumn,
+ PropertyId::ColumnWidthMode, PropertyId::ColumnWidthModeMenu,
+ PropertyId::RowHeaderWidth, PropertyId::TableFlags } );
return props;
}
diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp
index 9d863b5f8..20930e08e 100644
--- a/src/eepp/ui/css/stylesheetspecification.cpp
+++ b/src/eepp/ui/css/stylesheetspecification.cpp
@@ -336,6 +336,10 @@ void StyleSheetSpecification::registerDefaultProperties() {
.setType( PropertyType::NumberLength );
registerProperty( PropertyId::MainColumn, "main-column", "0" )
.setType( PropertyType::NumberInt );
+ registerProperty( PropertyId::ColumnWidthMode, "column-width-mode", "pixels" )
+ .setType( PropertyType::String );
+ registerProperty( PropertyId::ColumnWidthModeMenu, "column-width-mode-menu", "false" )
+ .setType( PropertyType::Bool );
registerProperty( PropertyId::RowHeaderWidth, "row-header-width", "" )
.setType( PropertyType::NumberLength );
registerProperty( PropertyId::TableFlags, "table-flags", "" )
@@ -590,6 +594,8 @@ void StyleSheetSpecification::registerDefaultProperties() {
.setRelativeTarget( PropertyRelativeTarget::LocalBlockWidth );
registerProperty( PropertyId::SplitterAlwaysShow, "splitter-always-show", "true" )
.setType( PropertyType::Bool );
+ registerProperty( PropertyId::SplitterHideOnEdge, "splitter-hide-on-edge", "false" )
+ .setType( PropertyType::Bool );
registerProperty( PropertyId::DroppableHoveringColor, "droppable-hovering-color", "#FFFFFF20" )
.setType( PropertyType::Color );
diff --git a/src/eepp/ui/tools/uicodeeditorsplitter.cpp b/src/eepp/ui/tools/uicodeeditorsplitter.cpp
index cdd1a79aa..8c2f0d93b 100644
--- a/src/eepp/ui/tools/uicodeeditorsplitter.cpp
+++ b/src/eepp/ui/tools/uicodeeditorsplitter.cpp
@@ -1926,6 +1926,8 @@ void UICodeEditorSplitter::onTabClosed( const TabEvent* tabEvent ) {
Node* remainingNode = tabWidget == splitter->getFirstWidget()
? splitter->getLastWidget()
: splitter->getFirstWidget();
+ // Keep the surviving subtree out of closeSplitter()'s recursive bookkeeping.
+ remainingNode->detach();
closeSplitter( splitter );
eeASSERT( parent->getChildCount() == 0 );
remainingNode->setParent( parent );
diff --git a/src/eepp/ui/tools/uitabwidgetsplitter.cpp b/src/eepp/ui/tools/uitabwidgetsplitter.cpp
index 7fc460016..9a651b9cf 100644
--- a/src/eepp/ui/tools/uitabwidgetsplitter.cpp
+++ b/src/eepp/ui/tools/uitabwidgetsplitter.cpp
@@ -17,7 +17,13 @@ UITabWidgetSplitter::UITabWidgetSplitter( UITabWidgetSplitter::Client* client,
UISceneNode* sceneNode ) :
mUISceneNode( sceneNode ), mClient( client ) {}
-UITabWidgetSplitter::~UITabWidgetSplitter() {}
+UITabWidgetSplitter::~UITabWidgetSplitter() {
+ mTabWidgetEventConnections.clear();
+ mWidgetEventConnections.clear();
+ mTabWidgets.clear();
+ mCurWidget = nullptr;
+ mClient = nullptr;
+}
UITabWidget* UITabWidgetSplitter::tabWidgetFromWidget( UIWidget* widget ) const {
if ( widget )
@@ -49,15 +55,15 @@ void UITabWidgetSplitter::setCurrentWidget( UIWidget* curWidget ) {
std::pair
UITabWidgetSplitter::createWidgetInTabWidget( UITabWidget* tabWidget, UIWidget* widget,
const std::string& tabName, bool focus ) {
- eeASSERT( curWidgetExists() );
if ( nullptr == tabWidget )
return std::make_pair( (UITab*)nullptr, (UIWidget*)nullptr );
UITab* tab = tabWidget->add( tabName, widget );
widget->setData( (UintPtr)tab );
- widget->on( Event::OnFocusWithin, [this]( const Event* event ) {
+ auto& connections = mWidgetEventConnections[widget];
+ connections += widget->connect( Event::OnFocusWithin, [this]( const Event* event ) {
setCurrentWidget( event->getNode()->asType() );
} );
- widget->on( Event::OnTitleChange, [this]( const Event* event ) {
+ connections += widget->connect( Event::OnTitleChange, [this]( const Event* event ) {
const TextEvent* tevent = static_cast( event );
UIWidget* widget = event->getNode()->asType();
UITabWidget* tabWidget = tabWidgetFromWidget( widget );
@@ -157,7 +163,8 @@ UITabWidget* UITabWidgetSplitter::createTabWidget( Node* parent ) {
},
mVisualSplitEdgePercent );
}
- tabWidget->on( Event::OnTabSelected, [this]( const Event* event ) {
+ auto& connections = mTabWidgetEventConnections[tabWidget];
+ connections += tabWidget->connect( Event::OnTabSelected, [this]( const Event* event ) {
UITabWidget* tabWidget = event->getNode()->asType();
eeASSERT( nullptr != tabWidget && nullptr != tabWidget->getTabSelected() &&
nullptr != tabWidget->getTabSelected()->getOwnedWidget() );
@@ -175,7 +182,7 @@ UITabWidget* UITabWidgetSplitter::createTabWidget( Node* parent ) {
}
return false;
} );
- tabWidget->on( Event::OnTabClosed, [this]( const Event* event ) {
+ connections += tabWidget->connect( Event::OnTabClosed, [this]( const Event* event ) {
onTabClosed( static_cast( event ) );
} );
if ( mOnTabWidgetCreateCb )
@@ -285,6 +292,7 @@ void UITabWidgetSplitter::closeTab( UIWidget* widget,
UITabWidget::FocusTabBehavior focusTabBehavior ) {
if ( widget ) {
UITabWidget* tabWidget = tabWidgetFromWidget( widget );
+ mWidgetEventConnections.erase( widget );
if ( tabWidget )
tabWidget->removeTab( (UITab*)widget->getData(), true, false, focusTabBehavior );
if ( mCurWidget == widget )
@@ -497,9 +505,12 @@ void UITabWidgetSplitter::closeTabWidgets( UISplitter* splitter ) {
Node* node = splitter->getFirstChild();
while ( node ) {
if ( node->isType( UI_TYPE_TABWIDGET ) ) {
- auto it =
- std::find( mTabWidgets.begin(), mTabWidgets.end(), node->asType() );
+ auto tabWidget = node->asType();
+ auto it = std::find( mTabWidgets.begin(), mTabWidgets.end(), tabWidget );
if ( it != mTabWidgets.end() ) {
+ if ( mOnTabWidgetCloseCb )
+ mOnTabWidgetCloseCb( tabWidget );
+ mTabWidgetEventConnections.erase( tabWidget );
Lock l( mTabWidgetMutex );
mTabWidgets.erase( it );
}
@@ -622,6 +633,9 @@ void UITabWidgetSplitter::onTabClosed( const TabEvent* tabEvent ) {
if ( tabWidget->getTabCount() == 0 ) {
UISplitter* splitter = splitterFromWidget( widget );
if ( splitter && splitter->isFull() ) {
+ if ( mOnTabWidgetCloseCb )
+ mOnTabWidgetCloseCb( tabWidget );
+ mTabWidgetEventConnections.erase( tabWidget );
tabWidget->close();
auto itWidget = std::find( mTabWidgets.begin(), mTabWidgets.end(), tabWidget );
if ( itWidget != mTabWidgets.end() ) {
@@ -647,6 +661,7 @@ void UITabWidgetSplitter::onTabClosed( const TabEvent* tabEvent ) {
Node* remainingNode = tabWidget == splitter->getFirstWidget()
? splitter->getLastWidget()
: splitter->getFirstWidget();
+ remainingNode->detach();
closeSplitter( splitter );
eeASSERT( parent->getChildCount() == 0 );
remainingNode->setParent( parent );
@@ -676,6 +691,10 @@ void UITabWidgetSplitter::setOnTabWidgetCreateCb( std::function cb ) {
+ mOnTabWidgetCloseCb = std::move( cb );
+}
+
bool UITabWidgetSplitter::getVisualSplitting() const {
return mVisualSplitting;
}
diff --git a/src/eepp/ui/uisplitter.cpp b/src/eepp/ui/uisplitter.cpp
index e3e3325be..9412e694f 100644
--- a/src/eepp/ui/uisplitter.cpp
+++ b/src/eepp/ui/uisplitter.cpp
@@ -12,29 +12,33 @@ UISplitter::UISplitter() :
UILayout( "splitter" ),
mOrientation( UIOrientation::Horizontal ),
mAlwaysShowSplitter( true ),
+ mHideSplitterOnEdge( false ),
mSplitPartition( StyleSheetLength( "50%" ) ),
mFirstWidget( NULL ),
mLastWidget( NULL ) {
mFlags |= UI_OWNS_CHILDREN_POSITION;
mSplitter = UIWidget::NewWithTag( "splitter::separator" );
mSplitter->setDragEnabled( true );
- mSplitter->on( Event::OnDragStart,
- [this]( const Event* ) { mSplitter->pushState( UIState::StateSelected ); } );
- mSplitter->on( Event::OnDragStop,
- [this]( const Event* ) { mSplitter->popState( UIState::StateSelected ); } );
+ mEventConnections += mSplitter->connect( Event::OnDragStart, [this]( const Event* ) {
+ mSplitter->pushState( UIState::StateSelected );
+ } );
+ mEventConnections += mSplitter->connect( Event::OnDragStop, [this]( const Event* ) {
+ mSplitter->popState( UIState::StateSelected );
+ } );
mSplitter->setParent( this );
mSplitter->setMinWidth( 4 );
mSplitter->setMinHeight( 4 );
- mSplitter->on( Event::OnSizeChange, [this]( const Event* ) {
+ mEventConnections += mSplitter->connect( Event::OnSizeChange, [this]( const Event* ) {
setLayoutDirty( LayoutInvalidation::ContainerLayout );
} );
- mSplitter->on( Event::MouseEnter, [this]( const Event* ) {
+ mEventConnections += mSplitter->connect( Event::MouseEnter, [this]( const Event* ) {
getUISceneNode()->setCursor( mOrientation == UIOrientation::Horizontal ? Cursor::SizeWE
: Cursor::SizeNS );
} );
- mSplitter->on( Event::MouseLeave,
- [this]( const Event* ) { getUISceneNode()->setCursor( Cursor::Arrow ); } );
- mSplitter->on( Event::OnPositionChange, [this]( const Event* ) {
+ mEventConnections += mSplitter->connect( Event::MouseLeave, [this]( const Event* ) {
+ getUISceneNode()->setCursor( Cursor::Arrow );
+ } );
+ mEventConnections += mSplitter->connect( Event::OnPositionChange, [this]( const Event* ) {
if ( mSplitter->isDragging() && !mDirtyLayout )
updateFromDrag();
} );
@@ -77,6 +81,17 @@ void UISplitter::setAlwaysShowSplitter( bool alwaysShowSplitter ) {
}
}
+const bool& UISplitter::hideSplitterOnEdge() const {
+ return mHideSplitterOnEdge;
+}
+
+void UISplitter::setHideSplitterOnEdge( bool hideSplitterOnEdge ) {
+ if ( hideSplitterOnEdge != mHideSplitterOnEdge ) {
+ mHideSplitterOnEdge = hideSplitterOnEdge;
+ setLayoutDirty( LayoutInvalidation::ContainerLayout );
+ }
+}
+
const StyleSheetLength& UISplitter::getSplitPartition() const {
return mSplitPartition;
}
@@ -127,6 +142,9 @@ bool UISplitter::applyProperty( const StyleSheetProperty& attribute ) {
case PropertyId::SplitterAlwaysShow:
setAlwaysShowSplitter( attribute.asBool() );
break;
+ case PropertyId::SplitterHideOnEdge:
+ setHideSplitterOnEdge( attribute.asBool() );
+ break;
case PropertyId::Orientation:
setOrientation( String::iequals( attribute.getValue(), "horizontal" )
? UIOrientation::Horizontal
@@ -149,6 +167,8 @@ std::string UISplitter::getPropertyString( const PropertyDefinition* propertyDef
return getSplitPartition().toString();
case PropertyId::SplitterAlwaysShow:
return alwaysShowSplitter() ? "true" : "false";
+ case PropertyId::SplitterHideOnEdge:
+ return hideSplitterOnEdge() ? "true" : "false";
case PropertyId::Orientation:
return getOrientation() == UIOrientation::Horizontal ? "horizontal" : "vertical";
default:
@@ -159,7 +179,7 @@ std::string UISplitter::getPropertyString( const PropertyDefinition* propertyDef
std::vector UISplitter::getPropertiesImplemented() const {
auto props = UIWidget::getPropertiesImplemented();
auto local = { PropertyId::SplitterPartition, PropertyId::SplitterAlwaysShow,
- PropertyId::Orientation };
+ PropertyId::SplitterHideOnEdge, PropertyId::Orientation };
props.insert( props.end(), local.begin(), local.end() );
return props;
}
@@ -204,7 +224,7 @@ void UISplitter::onChildCountChange( Node* child, const bool& removed ) {
void UISplitter::updateFromDrag() {
mDirtyLayout = true;
- mSplitter->setVisible( !mAlwaysShowSplitter && !mLastWidget ? false : true );
+ mSplitter->setVisible( shouldShowSplitter() );
mSplitter->setEnabled( mSplitter->isVisible() );
if ( UIOrientation::Horizontal == mOrientation ) {
@@ -351,7 +371,7 @@ void UISplitter::updateLayout() {
return;
}
- mSplitter->setVisible( !mAlwaysShowSplitter && !mLastWidget ? false : true );
+ mSplitter->setVisible( shouldShowSplitter() );
mSplitter->setEnabled( mSplitter->isVisible() );
Float totalSpace = mOrientation == UIOrientation::Horizontal
? mSize.getWidth() - mPaddingPx.Left - mPaddingPx.Right
@@ -426,6 +446,14 @@ void UISplitter::updateSplitterDragFlags() {
: UI_DRAG_HORIZONTAL );
}
+bool UISplitter::shouldShowSplitter() const {
+ if ( !mLastWidget )
+ return false;
+ return mAlwaysShowSplitter || !mHideSplitterOnEdge ||
+ mSplitPartition.getUnit() != StyleSheetLength::Percentage ||
+ ( mSplitPartition.getValue() > 0.f && mSplitPartition.getValue() < 100.f );
+}
+
Uint32 UISplitter::onMessage( const NodeMessage* Msg ) {
switch ( Msg->getMsg() ) {
case NodeMessage::LayoutAttributeChange: {
diff --git a/src/eepp/ui/uitableheadercolumn.cpp b/src/eepp/ui/uitableheadercolumn.cpp
index ed7256aa8..a09452a3d 100644
--- a/src/eepp/ui/uitableheadercolumn.cpp
+++ b/src/eepp/ui/uitableheadercolumn.cpp
@@ -1,4 +1,6 @@
#include
+#include
+#include
#include
#include
@@ -31,7 +33,7 @@ Uint32 UITableHeaderColumn::onCalculateDrag( const Vector2f& position, const Uin
}
Vector2f pos( eefloor( position.x ), eefloor( position.y ) );
if ( mDragPoint != pos && std::abs( mDragPoint.x - pos.x ) > 1.f ) {
- Sizef dragDiff( ( Float )( mDragPoint.x - pos.x ), 0 );
+ Sizef dragDiff( (Float)( mDragPoint.x - pos.x ), 0 );
if ( onDrag( pos, flags, dragDiff ) ) {
mDragPoint = pos;
eventDispatcher->setNodeDragging( this );
@@ -77,6 +79,30 @@ Uint32 UITableHeaderColumn::onMouseClick( const Vector2i& position, const Uint32
return UIPushButton::onMouseClick( position, flags );
}
+Uint32 UITableHeaderColumn::onMouseUp( const Vector2i& position, const Uint32& flags ) {
+ if ( ( flags & EE_BUTTON_RMASK ) && mView->isColumnWidthModeMenuEnabled() ) {
+ auto* menu = UIPopUpMenu::New();
+ menu->addRadioButton( i18n( "uitable_fit_columns_to_view", "Fit Columns to View" ),
+ mView->getColumnWidthMode() ==
+ UIAbstractTableView::ColumnWidthMode::Percentage )
+ ->setId( "percentage" );
+ menu->addRadioButton( i18n( "uitable_free_column_widths", "Free Column Widths" ),
+ mView->getColumnWidthMode() ==
+ UIAbstractTableView::ColumnWidthMode::Pixels )
+ ->setId( "pixels" );
+ menu->on( Event::OnItemClicked, [view = mView]( const Event* event ) {
+ if ( !event->getNode()->isType( UI_TYPE_MENUITEM ) )
+ return;
+ view->setColumnWidthMode( event->getNode()->getId() == "percentage"
+ ? UIAbstractTableView::ColumnWidthMode::Percentage
+ : UIAbstractTableView::ColumnWidthMode::Pixels );
+ } );
+ menu->setCloseOnHide( true );
+ menu->showAtScreenPosition( position.asFloat() );
+ }
+ return UIPushButton::onMouseUp( position, flags );
+}
+
Uint32 UITableHeaderColumn::onDrag( const Vector2f& position, const Uint32&,
const Sizef& dragDiff ) {
Vector2f localPos( convertToNodeSpace( position ) );
diff --git a/src/eepp/ui/uitabwidget.cpp b/src/eepp/ui/uitabwidget.cpp
index 3ea06fde8..8385c64dc 100644
--- a/src/eepp/ui/uitabwidget.cpp
+++ b/src/eepp/ui/uitabwidget.cpp
@@ -87,8 +87,10 @@ UITabWidget::UITabWidget() :
mTabScroll = UIScrollBar::NewHorizontalWithTag( "scrollbarmini" );
mTabScroll->setParent( mTabBar );
mTabScroll->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent );
- mTabScroll->on( Event::OnSizeChange, [this]( const Event* ) { updateScrollBar(); } );
- mTabScroll->on( Event::OnValueChange, [this]( const Event* ) { updateScroll(); } );
+ mEventConnections +=
+ mTabScroll->connect( Event::OnSizeChange, [this]( const Event* ) { updateScrollBar(); } );
+ mEventConnections +=
+ mTabScroll->connect( Event::OnValueChange, [this]( const Event* ) { updateScroll(); } );
onSizeChange();
@@ -560,6 +562,10 @@ UITab* UITabWidget::add( const String& text, UINode* nodeOwned, DrawablePtr icon
UITabWidget* UITabWidget::add( UITab* tab ) {
tab->setParent( mTabBar );
+ if ( tab->getOwnedWidget() && tab->getOwnedWidget()->isWidget() )
+ tab->getOwnedWidget()->asType()->setLayoutSizePolicy( SizePolicy::Fixed,
+ SizePolicy::Fixed );
+ refreshOwnedWidget( tab );
mTabs.push_back( tab );
diff --git a/src/tests/unit_tests/uitabwidgetsplitter_tests.cpp b/src/tests/unit_tests/uitabwidgetsplitter_tests.cpp
index e7eef7240..38ce45150 100644
--- a/src/tests/unit_tests/uitabwidgetsplitter_tests.cpp
+++ b/src/tests/unit_tests/uitabwidgetsplitter_tests.cpp
@@ -3,6 +3,8 @@
#include
#include
#include
+#include
+#include
#include
#include
@@ -21,6 +23,415 @@ class TestClient : public UITabWidgetSplitter::Client {
void onWidgetFocusChange( UIWidget* ) override { focusChangeCount++; }
};
+class TransferTestTabWidget : public UITabWidget {
+ public:
+ static TransferTestTabWidget* New() { return eeNew( TransferTestTabWidget, () ); }
+
+ void detachTab( UITab* tab ) { removeTab( tab, false, false, false ); }
+
+ const Sizef& getContainerPixelsSize() const { return mNodeContainer->getPixelsSize(); }
+};
+
+class ThreeColumnModel : public Model {
+ public:
+ explicit ThreeColumnModel( size_t rows = 1 ) : mRows( rows ) {}
+
+ size_t rowCount( const ModelIndex& = ModelIndex() ) const override { return mRows; }
+ size_t columnCount( const ModelIndex& = ModelIndex() ) const override { return 3; }
+ std::string columnName( const size_t& column ) const override {
+ return String::format( "Column %zu", column );
+ }
+ ModelIndex index( int row, int column,
+ const ModelIndex& parent = ModelIndex() ) const override {
+ return row == 0 && column >= 0 && column < 3 && !parent.isValid()
+ ? createIndex( row, column )
+ : ModelIndex{};
+ }
+ Variant data( const ModelIndex&, ModelRole = ModelRole::Display ) const override {
+ return "Value";
+ }
+
+ protected:
+ size_t mRows;
+};
+
+class PercentageTestTable : public UITableView {
+ public:
+ static PercentageTestTable* New() { return eeNew( PercentageTestTable, () ); }
+
+ void userResizeColumn( size_t column, Float width ) {
+ columnData( column ).setWidth( width, true );
+ onColumnSizeChange( column, true );
+ }
+
+ void setColumnMinimumWidth( size_t column, Float width ) {
+ columnData( column ).minWidth = width;
+ createOrUpdateColumns( false );
+ }
+
+ void resizeColumnToContent( size_t column, Float width ) {
+ mContentWidth = width;
+ onColumnResizeToContent( column );
+ }
+
+ Float getMaxColumnContentWidth( const size_t&, bool ) override { return mContentWidth; }
+
+ protected:
+ Float mContentWidth{ 0 };
+};
+
+UTEST( UISplitter, HideSplitterOnEdge ) {
+ UIApplication app(
+ WindowSettings( 800, 600, "eepp - unit tests" ),
+ UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
+
+ auto* splitter = UISplitter::New();
+ splitter->setParent( app.getUI() );
+ splitter->setPixelsSize( 800, 600 );
+ UIWidget::New()->setParent( splitter );
+ UIWidget::New()->setParent( splitter );
+ auto* separator = splitter->findByTag( "splitter::separator" );
+ ASSERT_TRUE( separator != nullptr );
+
+ splitter->setHideSplitterOnEdge( true );
+ splitter->setAlwaysShowSplitter( false );
+ splitter->setSplitPartition( StyleSheetLength( "100%" ) );
+ splitter->updateLayout();
+ EXPECT_FALSE( separator->isVisible() );
+ EXPECT_EQ( splitter->getFirstWidget()->getPixelsSize().getWidth(), 800.f );
+ EXPECT_EQ( splitter->getLastWidget()->getPixelsSize().getWidth(), 0.f );
+
+ splitter->setSplitPartition( StyleSheetLength( "75%" ) );
+ splitter->updateLayout();
+ app.getUI()->update( Milliseconds( 16 ) );
+ EXPECT_TRUE( separator->isVisible() );
+
+ splitter->setSplitPartition( StyleSheetLength( "0%" ) );
+ splitter->updateLayout();
+ EXPECT_FALSE( separator->isVisible() );
+ EXPECT_EQ( splitter->getFirstWidget()->getPixelsSize().getWidth(), 0.f );
+ EXPECT_EQ( splitter->getLastWidget()->getPixelsSize().getWidth(), 800.f );
+
+ splitter->setAlwaysShowSplitter( true );
+ splitter->updateLayout();
+ EXPECT_TRUE( separator->isVisible() );
+}
+
+UTEST( UISplitter, ControlsMatchParentChildWidth ) {
+ UIApplication app(
+ WindowSettings( 800, 600, "eepp - unit tests" ),
+ UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
+
+ auto* splitter = UISplitter::New();
+ splitter->setParent( app.getUI() );
+ splitter->setPixelsSize( 800, 600 );
+ auto* first = UIWidget::New();
+ first->setParent( splitter );
+ auto* last = UIWidget::New();
+ last->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent );
+ last->setParent( splitter );
+ splitter->setSplitPartition( StyleSheetLength( "75%" ) );
+ splitter->updateLayout();
+
+ EXPECT_TRUE( first->getPixelsSize().getWidth() > 590.f &&
+ first->getPixelsSize().getWidth() < 600.f );
+ EXPECT_TRUE( last->getPixelsSize().getWidth() > 190.f &&
+ last->getPixelsSize().getWidth() < 200.f );
+}
+
+UTEST( UIAbstractTableView, PercentageColumnWidthsScaleAndPreserveAdjacentTotal ) {
+ UIApplication app(
+ WindowSettings( 800, 600, "eepp - unit tests" ),
+ UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
+ auto* table = PercentageTestTable::New();
+ table->setParent( app.getUI() );
+ table->setPixelsSize( 400, 200 );
+ table->setModel( std::make_shared() );
+ table->setColumnWidth( 0, 100 );
+ table->setColumnWidth( 1, 200 );
+ table->setColumnWidth( 2, 100 );
+ table->setColumnWidthMode( UIAbstractTableView::ColumnWidthMode::Percentage );
+
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 0 ) - 25.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 1 ) - 50.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 2 ) - 25.f ) < 0.01f );
+
+ table->setPixelsSize( 800, 200 );
+ Float contentWidth = table->getContentSpaceWidth();
+ EXPECT_TRUE( std::abs( table->getColumnWidth( 0 ) - contentWidth * 0.25f ) < 1.f );
+ EXPECT_TRUE( std::abs( table->getColumnWidth( 1 ) - contentWidth * 0.5f ) < 1.f );
+ EXPECT_TRUE( std::abs( table->getColumnWidth( 2 ) - contentWidth * 0.25f ) < 1.f );
+
+ table->userResizeColumn( 0, contentWidth * 0.4f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 0 ) - 40.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 1 ) - 35.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 2 ) - 25.f ) < 0.01f );
+
+ table->setColumnWidthPercentage( 0, 70.f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 0 ) - 70.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 1 ) - 5.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 2 ) - 25.f ) < 0.01f );
+ EXPECT_TRUE( table->getColumnWidth( 0 ) >= 0 && table->getColumnWidth( 1 ) >= 0 &&
+ table->getColumnWidth( 2 ) >= 0 );
+}
+
+UTEST( UIAbstractTableView, PercentageColumnWidthsPreserveMinimumWidths ) {
+ UIApplication app(
+ WindowSettings( 800, 600, "eepp - unit tests" ),
+ UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
+ auto* table = PercentageTestTable::New();
+ table->setParent( app.getUI() );
+ table->setPixelsSize( 150, 200 );
+ table->setModel( std::make_shared() );
+ table->setColumnsWidthPercentage( { 25.f, 50.f, 25.f } );
+ table->setColumnMinimumWidth( 0, 80.f );
+ table->setColumnMinimumWidth( 1, 80.f );
+ table->setColumnMinimumWidth( 2, 80.f );
+ app.getUI()->update( Milliseconds( 16 ) );
+
+ EXPECT_TRUE( table->getColumnWidth( 0 ) >= 80.f );
+ EXPECT_TRUE( table->getColumnWidth( 1 ) >= 80.f );
+ EXPECT_TRUE( table->getColumnWidth( 2 ) >= 80.f );
+ EXPECT_TRUE( table->getColumnWidth( 0 ) + table->getColumnWidth( 1 ) +
+ table->getColumnWidth( 2 ) >
+ table->getContentSpaceWidth() );
+ EXPECT_TRUE( table->getHorizontalScrollBar()->isVisible() );
+}
+
+UTEST( UIAbstractTableView, PercentageColumnWidthsAcceptPartialAndSurplusValues ) {
+ UIApplication app(
+ WindowSettings( 800, 600, "eepp - unit tests" ),
+ UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
+ auto* table = PercentageTestTable::New();
+ table->setParent( app.getUI() );
+ table->setPixelsSize( 400, 200 );
+ table->setModel( std::make_shared() );
+
+ table->setColumnsWidthPercentage( { 20.f, 30.f } );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 0 ) - 20.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 1 ) - 30.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 2 ) - 50.f ) < 0.01f );
+
+ table->setColumnsWidthPercentage( { 10.f, 20.f, 70.f, 500.f } );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 0 ) - 10.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 1 ) - 20.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 2 ) - 70.f ) < 0.01f );
+
+ EXPECT_TRUE( table->unserializeColumnWidths( nlohmann::json{ 25.f, 25.f } ) );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 0 ) - 25.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 1 ) - 25.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 2 ) - 50.f ) < 0.01f );
+}
+
+UTEST( UIAbstractTableView, PercentageColumnResizeToContentPreservesAdjacentTotal ) {
+ UIApplication app(
+ WindowSettings( 800, 600, "eepp - unit tests" ),
+ UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
+ auto* table = PercentageTestTable::New();
+ table->setParent( app.getUI() );
+ table->setPixelsSize( 400, 200 );
+ table->setModel( std::make_shared() );
+ table->setColumnsWidthPercentage( { 25.f, 50.f, 25.f } );
+ const Float contentWidth = table->getContentSpaceWidth();
+
+ table->resizeColumnToContent( 0, contentWidth * 0.4f );
+
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 0 ) - 40.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 1 ) - 35.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 2 ) - 25.f ) < 0.01f );
+
+ table->setColumnsWidthPercentage( { 25.f, 50.f, 25.f } );
+ table->setColumnMinimumWidth( 1, contentWidth * 0.3f );
+ table->resizeColumnToContent( 0, contentWidth * 0.6f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 0 ) - 45.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 1 ) - 30.f ) < 0.01f );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 2 ) - 25.f ) < 0.01f );
+}
+
+UTEST( UIAbstractTableView, ColumnWidthModeAndMenuCanBeConfiguredFromXML ) {
+ UIApplication app(
+ WindowSettings( 800, 600, "eepp - unit tests" ),
+ UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
+ auto* defaultTable =
+ app.getUI()->loadLayoutFromString( R"xml()xml" )->asType();
+ EXPECT_FALSE( defaultTable->isColumnWidthModeMenuEnabled() );
+ auto* table = app.getUI()
+ ->loadLayoutFromString( R"xml()xml" )
+ ->asType();
+ EXPECT_TRUE( table->getColumnWidthMode() == UIAbstractTableView::ColumnWidthMode::Percentage );
+ EXPECT_TRUE( table->isColumnWidthModeMenuEnabled() );
+ auto* cssRoot = app.getUI()->loadLayoutFromString( R"xml(
+
+
+
+ )xml" );
+ auto* cssTable = cssRoot->querySelector( "#css_table" )->asType();
+ EXPECT_TRUE( cssTable->isColumnWidthModeMenuEnabled() );
+}
+
+UTEST( UIAbstractTableView, PercentageRoundingDoesNotCreatePhantomHorizontalScroll ) {
+ Float previousDensity = PixelDensity::getPixelDensity();
+ PixelDensity::setPixelDensity( 1.5f );
+ UIApplication app(
+ WindowSettings( 828, 300, "eepp - unit tests" ),
+ UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
+ auto* table = PercentageTestTable::New();
+ table->setParent( app.getUI() );
+ table->setPixelsSize( 828, 300 );
+ table->setModel( std::make_shared() );
+ table->setColumnsWidthPercentage( { 100.f / 3.f, 100.f / 3.f, 100.f / 3.f } );
+ app.getUI()->update( Milliseconds( 16 ) );
+
+ EXPECT_EQ( table->getColumnWidth( 0 ) + table->getColumnWidth( 1 ) + table->getColumnWidth( 2 ),
+ table->getContentSpaceWidth() );
+ EXPECT_FALSE( table->getHorizontalScrollBar()->isVisible() );
+
+ table->setColumnsWidthPercentage( { 25.f, 50.f, 25.f } );
+ table->setColumnMinimumWidth( 2, 208.f );
+ app.getUI()->update( Milliseconds( 16 ) );
+ EXPECT_EQ( table->getColumnWidth( 0 ) + table->getColumnWidth( 1 ) + table->getColumnWidth( 2 ),
+ table->getContentSpaceWidth() );
+ EXPECT_TRUE( table->getColumnWidth( 2 ) >= 208.f );
+ EXPECT_FALSE( table->getHorizontalScrollBar()->isVisible() );
+ PixelDensity::setPixelDensity( previousDensity );
+}
+
+UTEST( UIAbstractTableView, PercentageColumnsRecomputeWhenVerticalScrollbarAppears ) {
+ UIApplication app(
+ WindowSettings( 800, 600, "eepp - unit tests" ),
+ UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
+ auto* table = PercentageTestTable::New();
+ table->setParent( app.getUI() );
+ table->setPixelsSize( 400, 100 );
+ table->setModel( std::make_shared( 100 ) );
+ table->setColumnsWidthPercentage( { 25.f, 50.f, 25.f } );
+ app.getUI()->update( Milliseconds( 16 ) );
+
+ EXPECT_TRUE( table->getVerticalScrollBar()->isVisible() );
+ EXPECT_EQ( table->getColumnWidth( 0 ) + table->getColumnWidth( 1 ) + table->getColumnWidth( 2 ),
+ table->getContentSpaceWidth() );
+ EXPECT_FALSE( table->getHorizontalScrollBar()->isVisible() );
+}
+
+UTEST( UIAbstractTableView, AutoPixelColumnsRecomputeWhenVerticalScrollbarAppears ) {
+ UIApplication app(
+ WindowSettings( 800, 600, "eepp - unit tests" ),
+ UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
+ auto* table = PercentageTestTable::New();
+ table->setParent( app.getUI() );
+ table->setPixelsSize( 400, 100 );
+ table->setModel( std::make_shared( 100 ) );
+ table->setAutoColumnsWidth( true );
+ app.getUI()->update( Milliseconds( 16 ) );
+
+ EXPECT_TRUE( table->getColumnWidthMode() == UIAbstractTableView::ColumnWidthMode::Pixels );
+ EXPECT_TRUE( table->getVerticalScrollBar()->isVisible() );
+ EXPECT_FALSE( table->getHorizontalScrollBar()->isVisible() );
+}
+
+UTEST( UIAbstractTableView, ColumnWidthsSerializationRoundTripsBothModes ) {
+ UIApplication app(
+ WindowSettings( 800, 600, "eepp - unit tests" ),
+ UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
+ auto* table = PercentageTestTable::New();
+ table->setParent( app.getUI() );
+ table->setPixelsSize( 400, 200 );
+ table->setModel( std::make_shared() );
+ table->setColumnsWidthPercentage( { 20.f, 30.f, 50.f } );
+ auto percentageWidths = table->serializeColumnWidths();
+ table->setColumnWidthMode( UIAbstractTableView::ColumnWidthMode::Pixels );
+ EXPECT_TRUE( table->unserializeColumnWidths( percentageWidths ) );
+ EXPECT_TRUE( table->getColumnWidthMode() == UIAbstractTableView::ColumnWidthMode::Percentage );
+ EXPECT_TRUE( std::abs( table->getColumnWidthPercentage( 0 ) - 20.f ) < 0.01f );
+
+ table->setColumnWidthMode( UIAbstractTableView::ColumnWidthMode::Pixels );
+ table->setColumnWidth( 0, PixelDensity::dpToPx( 80.f ) );
+ table->setColumnWidth( 1, PixelDensity::dpToPx( 120.f ) );
+ table->setColumnWidth( 2, PixelDensity::dpToPx( 160.f ) );
+ auto pixelWidths = table->serializeColumnWidths();
+ table->setColumnsWidthPercentage( { 25.f, 50.f, 25.f } );
+ EXPECT_TRUE( table->unserializeColumnWidths( pixelWidths ) );
+ EXPECT_TRUE( table->getColumnWidthMode() == UIAbstractTableView::ColumnWidthMode::Pixels );
+ EXPECT_EQ( table->getColumnWidth( 0 ), PixelDensity::dpToPx( 80.f ) );
+ EXPECT_EQ( table->getColumnWidth( 1 ), PixelDensity::dpToPx( 120.f ) );
+ EXPECT_EQ( table->getColumnWidth( 2 ), PixelDensity::dpToPx( 160.f ) );
+
+ EXPECT_TRUE( table->unserializeColumnWidths( nlohmann::json{ 25.f, 50.f, 25.f } ) );
+ EXPECT_TRUE( table->getColumnWidthMode() == UIAbstractTableView::ColumnWidthMode::Percentage );
+
+ auto* deferredTable = PercentageTestTable::New();
+ deferredTable->setParent( app.getUI() );
+ deferredTable->setPixelsSize( 400, 200 );
+ EXPECT_TRUE( deferredTable->unserializeColumnWidths( percentageWidths ) );
+ EXPECT_TRUE( deferredTable->serializeColumnWidths() == percentageWidths );
+ deferredTable->setModel( std::make_shared() );
+ EXPECT_TRUE( deferredTable->getColumnWidthMode() ==
+ UIAbstractTableView::ColumnWidthMode::Percentage );
+ EXPECT_TRUE( std::abs( deferredTable->getColumnWidthPercentage( 0 ) - 20.f ) < 0.01f );
+}
+
+UTEST( UITabWidget, TransferTabPreservesDestinationTabsAndOwnedWidgets ) {
+ UIApplication app(
+ WindowSettings( 800, 600, "eepp - unit tests" ),
+ UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
+
+ auto* source = TransferTestTabWidget::New();
+ source->setParent( app.getUI() );
+ auto* destination = TransferTestTabWidget::New();
+ destination->setParent( app.getUI() );
+ destination->setPixelsSize( 240, 180 );
+ auto* sourceWidget = UIWidget::New();
+ auto* destinationWidgetA = UIWidget::New();
+ auto* destinationWidgetB = UIWidget::New();
+ auto* movedTab = source->add( "Moved", sourceWidget );
+ destination->add( "Existing A", destinationWidgetA );
+ destination->add( "Existing B", destinationWidgetB );
+
+ source->detachTab( movedTab );
+ destination->add( movedTab );
+ destination->setTabSelected( movedTab );
+
+ EXPECT_EQ( source->getTabCount(), Uint32{ 0 } );
+ EXPECT_EQ( destination->getTabCount(), Uint32{ 3 } );
+ EXPECT_TRUE( destination->getTab( 0 )->getOwnedWidget() == destinationWidgetA );
+ EXPECT_TRUE( destination->getTab( 1 )->getOwnedWidget() == destinationWidgetB );
+ EXPECT_TRUE( destination->getTab( 2 )->getOwnedWidget() == sourceWidget );
+ EXPECT_TRUE( destination->isParentOf( sourceWidget ) );
+ EXPECT_TRUE( destination->isParentOf( destinationWidgetA ) );
+ EXPECT_TRUE( destination->isParentOf( destinationWidgetB ) );
+ EXPECT_TRUE( sourceWidget->getPixelsSize() == destination->getContainerPixelsSize() );
+ EXPECT_TRUE( sourceWidget->isVisible() );
+ EXPECT_FALSE( destinationWidgetA->isVisible() );
+ EXPECT_FALSE( destinationWidgetB->isVisible() );
+}
+
+UTEST( UITabWidgetSplitter, ClosingSplitNotifiesBeforeTabWidgetDestruction ) {
+ UIApplication app(
+ WindowSettings( 800, 600, "eepp - unit tests" ),
+ UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
+ TestClient client;
+ auto* splitter = UITabWidgetSplitter::New( &client, app.getUI() );
+ auto* container = UILayout::New();
+ container->setParent( app.getUI() );
+ auto* firstTabWidget = splitter->createTabWidget( container );
+ auto* firstWidget = UIWidget::New();
+ splitter->createWidgetInTabWidget( firstTabWidget, firstWidget, "First" );
+ auto* secondTabWidget = splitter->splitTabWidget( SplitDirection::Right, firstTabWidget );
+ auto* secondWidget = UIWidget::New();
+ splitter->createWidgetInTabWidget( secondTabWidget, secondWidget, "Second" );
+ UITabWidget* closedTabWidget = nullptr;
+ splitter->setOnTabWidgetCloseCb(
+ [&closedTabWidget]( UITabWidget* tabWidget ) { closedTabWidget = tabWidget; } );
+
+ splitter->closeTab( secondWidget, UITabWidget::FocusTabBehavior::Default );
+ app.getUI()->update( Milliseconds( 16 ) );
+
+ EXPECT_EQ( closedTabWidget, secondTabWidget );
+ EXPECT_EQ( splitter->getTabWidgets().size(), 1UL );
+ eeDelete( splitter );
+}
+
UTEST( UITabWidgetSplitter, Serialization ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - unit tests" ),
@@ -34,15 +445,14 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* tabWidget = splitter->createTabWidget( (Node*)app.getUI() );
bool saved = false;
- splitter->registerWidgetType(
- "testwidget",
- { [&]( UIWidget* ) {
- saved = true;
- return nlohmann::json{ { "custom_key", 42 } };
- },
- []( const nlohmann::json& ) -> WidgetLoadResult {
- return { nullptr, nullptr, "" };
- } } );
+ splitter->registerWidgetType( "testwidget",
+ { [&]( UIWidget* ) {
+ saved = true;
+ return nlohmann::json{ { "custom_key", 42 } };
+ },
+ []( const nlohmann::json& ) -> WidgetLoadResult {
+ return { nullptr, nullptr, "" };
+ } } );
auto* w = UIWidget::New();
w->addClass( "testwidget" );
@@ -93,9 +503,8 @@ UTEST( UITabWidgetSplitter, Serialization ) {
splitter->createWidgetInTabWidget( tabWidget, ghost, "Ghost" );
splitter->registerWidgetType(
- "known",
- { []( UIWidget* ) { return nlohmann::json{ { "val", 1 } }; },
- []( const nlohmann::json& ) -> WidgetLoadResult { return {}; } } );
+ "known", { []( UIWidget* ) { return nlohmann::json{ { "val", 1 } }; },
+ []( const nlohmann::json& ) -> WidgetLoadResult { return {}; } } );
auto* reg = UIWidget::New();
reg->addClass( "known" );
@@ -117,13 +526,12 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* tabWidget = splitter->createTabWidget( (Node*)app.getUI() );
splitter->registerWidgetType(
- "panewidget",
- { []( UIWidget* ) { return nlohmann::json{ { "pane", true } }; },
- []( const nlohmann::json& j ) -> WidgetLoadResult {
- auto* w = UIWidget::New();
- w->addClass( "panewidget" );
- return { w, nullptr, j.value( "title", "" ) };
- } } );
+ "panewidget", { []( UIWidget* ) { return nlohmann::json{ { "pane", true } }; },
+ []( const nlohmann::json& j ) -> WidgetLoadResult {
+ auto* w = UIWidget::New();
+ w->addClass( "panewidget" );
+ return { w, nullptr, j.value( "title", "" ) };
+ } } );
auto* w1 = UIWidget::New();
w1->addClass( "panewidget" );
@@ -163,13 +571,12 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* tab1 = splitter1->createTabWidget( (Node*)app.getUI() );
splitter1->registerWidgetType(
- "mywidget",
- { []( UIWidget* ) { return nlohmann::json{ { "data", "hello" } }; },
- []( const nlohmann::json& j ) -> WidgetLoadResult {
- auto* w = UIWidget::New();
- w->addClass( "mywidget" );
- return { w, nullptr, j.value( "title", "" ) };
- } } );
+ "mywidget", { []( UIWidget* ) { return nlohmann::json{ { "data", "hello" } }; },
+ []( const nlohmann::json& j ) -> WidgetLoadResult {
+ auto* w = UIWidget::New();
+ w->addClass( "mywidget" );
+ return { w, nullptr, j.value( "title", "" ) };
+ } } );
auto* wa = UIWidget::New();
wa->addClass( "mywidget" );
@@ -186,14 +593,13 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* splitter2 = UITabWidgetSplitter::New( &client2, app.getUI() );
auto* tab2 = splitter2->createTabWidget( (Node*)app.getUI() );
- splitter2->registerWidgetType(
- "mywidget",
- { []( UIWidget* ) { return nlohmann::json::object(); },
- []( const nlohmann::json& j ) -> WidgetLoadResult {
- auto* w = UIWidget::New();
- w->addClass( "mywidget" );
- return { w, nullptr, j.value( "title", "" ) };
- } } );
+ splitter2->registerWidgetType( "mywidget",
+ { []( UIWidget* ) { return nlohmann::json::object(); },
+ []( const nlohmann::json& j ) -> WidgetLoadResult {
+ auto* w = UIWidget::New();
+ w->addClass( "mywidget" );
+ return { w, nullptr, j.value( "title", "" ) };
+ } } );
splitter2->fromJSON( saved );
@@ -256,14 +662,13 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* splitter1 = UITabWidgetSplitter::New( &client1, app.getUI() );
auto* tab1 = splitter1->createTabWidget( (Node*)app.getUI() );
- splitter1->registerWidgetType(
- "partwidget",
- { []( UIWidget* ) { return nlohmann::json::object(); },
- []( const nlohmann::json& j ) -> WidgetLoadResult {
- auto* w = UIWidget::New();
- w->addClass( "partwidget" );
- return { w, nullptr, j.value( "title", "" ) };
- } } );
+ splitter1->registerWidgetType( "partwidget",
+ { []( UIWidget* ) { return nlohmann::json::object(); },
+ []( const nlohmann::json& j ) -> WidgetLoadResult {
+ auto* w = UIWidget::New();
+ w->addClass( "partwidget" );
+ return { w, nullptr, j.value( "title", "" ) };
+ } } );
auto* w1 = UIWidget::New();
w1->addClass( "partwidget" );
@@ -287,14 +692,13 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* splitter2 = UITabWidgetSplitter::New( &client2, app.getUI() );
splitter2->createTabWidget( (Node*)app.getUI() );
- splitter2->registerWidgetType(
- "partwidget",
- { []( UIWidget* ) { return nlohmann::json::object(); },
- []( const nlohmann::json& j ) -> WidgetLoadResult {
- auto* w = UIWidget::New();
- w->addClass( "partwidget" );
- return { w, nullptr, j.value( "title", "" ) };
- } } );
+ splitter2->registerWidgetType( "partwidget",
+ { []( UIWidget* ) { return nlohmann::json::object(); },
+ []( const nlohmann::json& j ) -> WidgetLoadResult {
+ auto* w = UIWidget::New();
+ w->addClass( "partwidget" );
+ return { w, nullptr, j.value( "title", "" ) };
+ } } );
splitter2->fromJSON( saved );
diff --git a/src/tools/ecode/appconfig.cpp b/src/tools/ecode/appconfig.cpp
index 302141ea7..580ea7114 100644
--- a/src/tools/ecode/appconfig.cpp
+++ b/src/tools/ecode/appconfig.cpp
@@ -121,6 +121,7 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath,
windowState.winIcon = ini.getValue( "window", "winicon", resPath + "icon/ecode.png" );
windowState.panelPartition = iniState.getValue( "window", "panel_partition", "15%" );
windowState.statusBarPartition = iniState.getValue( "window", "status_bar_partition", "85%" );
+ windowState.rightPanelPartition = iniState.getValue( "window", "right_panel_partition", "75%" );
windowState.displayIndex = iniState.getValueI( "window", "display_index", 0 );
windowState.position.x = iniState.getValueI( "window", "x", -1 );
windowState.position.y = iniState.getValueI( "window", "y", -1 );
@@ -331,6 +332,7 @@ void AppConfig::save( const std::vector& recentFiles,
iniState.setValueF( "window", "pixeldensity", windowState.pixelDensity );
iniState.setValue( "window", "panel_partition", panelPartition );
iniState.setValue( "window", "status_bar_partition", statusBarPartition );
+ iniState.setValue( "window", "right_panel_partition", windowState.rightPanelPartition );
iniState.setValueI( "window", "display_index", windowState.displayIndex );
iniState.setValueI( "window", "x", windowState.position.x );
iniState.setValueI( "window", "y", windowState.position.y );
diff --git a/src/tools/ecode/appconfig.hpp b/src/tools/ecode/appconfig.hpp
index ce5abdf75..e9d653289 100644
--- a/src/tools/ecode/appconfig.hpp
+++ b/src/tools/ecode/appconfig.hpp
@@ -90,6 +90,7 @@ struct WindowStateConfig {
bool maximized{ false };
std::string panelPartition;
std::string statusBarPartition;
+ std::string rightPanelPartition{ "75%" };
int displayIndex{ 0 };
Vector2i position{ -1, -1 };
Uint32 lastRunVersion{ 0 };
diff --git a/src/tools/ecode/applayout.xml.hpp b/src/tools/ecode/applayout.xml.hpp
index 2150269dc..355add5fb 100644
--- a/src/tools/ecode/applayout.xml.hpp
+++ b/src/tools/ecode/applayout.xml.hpp
@@ -615,6 +615,7 @@ TabWidget::container > ImageViewer > TextView {
+
@@ -706,6 +707,8 @@ TabWidget::container > ImageViewer > TextView {
+
+