From bf750761ae5fd2aecf9e1ff6e008d84db04d7356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Fri, 14 Aug 2026 00:17:29 -0300 Subject: [PATCH] ecode: add customizable debugger panel layouts - add a reusable right-side panel managed at the application level - migrate debugger tabs to UITabWidgetSplitter instances - support moving and splitting debugger tabs across bottom and right panels - restrict debugger tab drops to debugger-owned panel targets - serialize debugger tab layouts, panel state, and table column widths - add pixel and percentage column-width modes to UIAbstractTableView - support XML/CSS configuration and an optional width-mode context menu - preserve percentage totals across resizing, fit-to-content, minimum widths, scrollbar changes, and pixel-density rounding - add splitter edge hiding while preserving splitter-always-show precedence - use EventConnection ownership for safe widget and controller lifetimes - validate serialized splitter layouts with structural limits - display project-relative source paths in debugger stack frames - add ecode localization strings, CSS documentation, tests, and agent rules --- .agent/SOUL.md | 11 + .agent/rules/project-introduction.md | 15 + .agent/rules/unit-tests.md | 10 +- bin/assets/i18n/de.xml | 2 + bin/assets/i18n/en.xml | 2 + bin/assets/i18n/fr.xml | 2 + bin/assets/i18n/zh.xml | 2 + docs/articles/cssspecification.md | 39 +- .../eepp/ui/abstract/uiabstracttableview.hpp | 40 ++ include/eepp/ui/css/propertyids.hpp | 3 + include/eepp/ui/tools/uitabwidgetsplitter.hpp | 9 +- include/eepp/ui/uisplitter.hpp | 9 + include/eepp/ui/uitableheadercolumn.hpp | 4 +- include/eepp/ui/uitabwidget.hpp | 2 + src/eepp/ui/abstract/uiabstracttableview.cpp | 299 ++++++++++- src/eepp/ui/css/stylesheetspecification.cpp | 6 + src/eepp/ui/tools/uicodeeditorsplitter.cpp | 2 + src/eepp/ui/tools/uitabwidgetsplitter.cpp | 35 +- src/eepp/ui/uisplitter.cpp | 52 +- src/eepp/ui/uitableheadercolumn.cpp | 28 +- src/eepp/ui/uitabwidget.cpp | 10 +- .../unit_tests/uitabwidgetsplitter_tests.cpp | 504 ++++++++++++++++-- src/tools/ecode/appconfig.cpp | 2 + src/tools/ecode/appconfig.hpp | 1 + src/tools/ecode/applayout.xml.hpp | 3 + src/tools/ecode/ecode.cpp | 21 + src/tools/ecode/ecode.hpp | 7 + .../debugger/debuggerclientlistener.cpp | 6 +- .../ecode/plugins/debugger/debuggerplugin.cpp | 19 +- .../ecode/plugins/debugger/debuggerplugin.hpp | 2 + .../plugins/debugger/models/stackmodel.cpp | 31 +- .../plugins/debugger/models/stackmodel.hpp | 7 +- .../debugger/statusdebuggercontroller.cpp | 327 ++++++++++-- .../debugger/statusdebuggercontroller.hpp | 40 +- .../ecode/plugins/plugincontextprovider.hpp | 7 + src/tools/ecode/uirightpanel.cpp | 101 ++++ src/tools/ecode/uirightpanel.hpp | 50 ++ 37 files changed, 1579 insertions(+), 131 deletions(-) create mode 100644 src/tools/ecode/uirightpanel.cpp create mode 100644 src/tools/ecode/uirightpanel.hpp 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 { + + saveState(); mConfig.save( mRecentFiles, mRecentFolders, @@ -3810,6 +3814,17 @@ UISplitter* App::getMainSplitter() const { return mMainSplitter; } +UIRightPanel* App::getRightPanel() const { + return mRightPanel.get(); +} + +StatusDebuggerController* App::getStatusDebuggerController() const { + if ( !mStatusBar ) + return nullptr; + auto element = mStatusBar->getStatusBarElement( "status_app_debugger" ); + return static_cast( element.get() ); +} + StatusTerminalController* App::getStatusTerminalController() const { return mStatusTerminalController.get(); } @@ -4979,6 +4994,8 @@ void App::init( InitParameters& params ) { mSplitter->setOpenDocumentsInMainSplit( mConfig.editor.openDocumentsInMainSplit ); mSplitter->setRestoreEditorSelectionOnFocus( mConfig.editor.restoreEditorSelectionOnFocus ); mSplitter->setOnTabWidgetCreateCb( [this]( UITabWidget* tabWidget ) { + tabWidget->setAcceptsDropOfWidgetFn( + []( const UIWidget* widget ) { return !widget->hasClass( "debugger-tab" ); } ); tabWidget->getTabBar()->onDoubleClick( [this]( const MouseEvent* ) { mSplitter->createEditorInNewTab(); } ); } ); @@ -5032,6 +5049,10 @@ void App::init( InitParameters& params ) { mMainSplitter = mUISceneNode->find( "main_splitter" ); mMainSplitter->setSplitPartition( StyleSheetLength( mConfig.windowState.statusBarPartition ) ); + auto rightPanelSplitter = mUISceneNode->find( "right_panel_splitter" ); + auto rightPanelContainer = mUISceneNode->find( "right_panel_container" ); + mRightPanel = + std::make_unique( rightPanelSplitter, rightPanelContainer, &mConfig ); mStatusBar = mUISceneNode->find( "status_bar" ); mPluginManager->setMainSplitter( mMainSplitter ); diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index 51282e289..90dcb6bf2 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -35,6 +35,8 @@ class DateTimeController; class FontPickerController; class SettingsMenu; class UITreeViewFS; +class StatusDebuggerController; +class UIRightPanel; class App : public UICodeEditorSplitter::Client, public PluginContextProvider { public: @@ -599,6 +601,10 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { UISplitter* getMainSplitter() const; + UIRightPanel* getRightPanel() const; + + StatusDebuggerController* getStatusDebuggerController() const; + StatusTerminalController* getStatusTerminalController() const; void hideStatusTerminal(); @@ -777,6 +783,7 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { UIThemePtr mTheme; UIStatusBar* mStatusBar{ nullptr }; UISplitter* mMainSplitter{ nullptr }; + std::unique_ptr mRightPanel; UIMessageBox* mCloseMsgBox{ nullptr }; UIMenuBar* mMenuBar{ nullptr }; std::unique_ptr mSettingsActions; diff --git a/src/tools/ecode/plugins/debugger/debuggerclientlistener.cpp b/src/tools/ecode/plugins/debugger/debuggerclientlistener.cpp index 6db2c81e4..f9d7faf1d 100644 --- a/src/tools/ecode/plugins/debugger/debuggerclientlistener.cpp +++ b/src/tools/ecode/plugins/debugger/debuggerclientlistener.cpp @@ -140,8 +140,10 @@ void DebuggerClientListener::initUI() { } if ( !mStackModel ) { - mStackModel = std::make_shared( StackTraceInfo{}, sceneNode ); - } + mStackModel = + std::make_shared( StackTraceInfo{}, sceneNode, mPlugin->mProjectPath ); + } else + mStackModel->setProjectPath( mPlugin->mProjectPath ); UITableView* uiThreads = sdc->getUIThreads(); uiThreads->setModel( mThreadsModel ); diff --git a/src/tools/ecode/plugins/debugger/debuggerplugin.cpp b/src/tools/ecode/plugins/debugger/debuggerplugin.cpp index 36afeec83..b97a34d79 100644 --- a/src/tools/ecode/plugins/debugger/debuggerplugin.cpp +++ b/src/tools/ecode/plugins/debugger/debuggerplugin.cpp @@ -183,6 +183,11 @@ DebuggerPlugin::~DebuggerPlugin() { } } +void DebuggerPlugin::onSaveState( IniFile* state ) { + if ( auto controller = getStatusDebuggerController() ) + state->setValue( "debugger", "panel_layout", controller->saveLayout() ); +} + void DebuggerPlugin::onSaveProject( const std::string& /*projectFolder*/, const std::string& projectStatePath, bool rewriteStateOnlyIfNeeded ) { @@ -579,11 +584,11 @@ void DebuggerPlugin::loadDAPConfig( const std::string& path, bool updateConfigFi mKeyBindings["debugger-step-over"] = "f10"; mKeyBindings["debugger-step-into"] = "f11"; mKeyBindings["debugger-step-out"] = "shift+f11"; - #if EE_PLATFORM == EE_PLATFORM_MACOS +#if EE_PLATFORM == EE_PLATFORM_MACOS mKeyBindings["toggle-status-app-debugger"] = "mod+6"; - #else +#else mKeyBindings["toggle-status-app-debugger"] = "alt+6"; - #endif +#endif } if ( j.contains( "keybindings" ) ) { @@ -768,8 +773,9 @@ void DebuggerPlugin::buildSidePanelTab() { if ( mProjectPath.empty() ) return; UIIcon* icon = findIcon( "debug" ); - mTab = mSidePanel->add( i18n( "debugger", "Debugger" ), mTabContents, - icon ? icon->createDrawable( PixelDensity::dpToPx( 12 ) ) : nullptr ); + mTab = + mSidePanel->add( i18n( "debugger", "Debugger" ), mTabContents, + icon ? icon->createDrawable( PixelDensity::dpToPx( 12 ) ) : nullptr ); mTab->setId( "debugger_tab" ); mTab->setTextAsFallback( true ); @@ -1217,8 +1223,7 @@ bool DebuggerPlugin::replaceInVal( std::string& val, String::replaceAll( val, KEY_PATH_SEPARATOR, FileSystem::getOSSlash() ); String::replaceAll( val, KEY_PATH_SEPARATOR_ABBR, FileSystem::getOSSlash() ); String::replaceAll( val, KEY_UUID, UUID().toString() ); - String::replaceAll( val, KEY_TIMESTAMP, - std::to_string( Sys::getUnixTimestamp() ) ); + String::replaceAll( val, KEY_TIMESTAMP, std::to_string( Sys::getUnixTimestamp() ) ); auto* editor = getPluginContext()->getSplitter()->getCurEditor(); if ( getPluginContext()->getSplitter()->getCurEditor() ) { diff --git a/src/tools/ecode/plugins/debugger/debuggerplugin.hpp b/src/tools/ecode/plugins/debugger/debuggerplugin.hpp index 45a7df3bd..d02802912 100644 --- a/src/tools/ecode/plugins/debugger/debuggerplugin.hpp +++ b/src/tools/ecode/plugins/debugger/debuggerplugin.hpp @@ -67,6 +67,8 @@ class DebuggerPlugin : public PluginBase { std::string getDescription() override { return Definition().description; } + void onSaveState( IniFile* state ) override; + void onSaveProject( const std::string& projectFolder, const std::string& projectStatePath, bool rewriteStateOnlyIfNeeded ) override; diff --git a/src/tools/ecode/plugins/debugger/models/stackmodel.cpp b/src/tools/ecode/plugins/debugger/models/stackmodel.cpp index 4881f3b3c..6913e8277 100644 --- a/src/tools/ecode/plugins/debugger/models/stackmodel.cpp +++ b/src/tools/ecode/plugins/debugger/models/stackmodel.cpp @@ -1,10 +1,15 @@ #include "stackmodel.hpp" +#include #include namespace ecode { -StackModel::StackModel( StackTraceInfo&& stack, UISceneNode* sceneNode ) : - mStack( std::move( stack ) ), mSceneNode( sceneNode ) {} +StackModel::StackModel( StackTraceInfo&& stack, UISceneNode* sceneNode, std::string projectPath ) : + mStack( std::move( stack ) ), + mProjectPath( std::move( projectPath ) ), + mSceneNode( sceneNode ) { + FileSystem::dirAddSlashAtEnd( mProjectPath ); +} size_t StackModel::rowCount( const ModelIndex& ) const { Lock l( mResourceLock ); @@ -45,7 +50,8 @@ Variant StackModel::data( const ModelIndex& modelIndex, ModelRole role ) const { : Variant(); case Columns::SourcePath: return mStack.stackFrames[modelIndex.row()].source - ? Variant( mStack.stackFrames[modelIndex.row()].source->path ) + ? Variant( displaySourcePath( + mStack.stackFrames[modelIndex.row()].source->path ) ) : Variant(); case Columns::Line: return Variant( String::toString( mStack.stackFrames[modelIndex.row()].line ) ); @@ -90,4 +96,23 @@ void StackModel::setCurrentScopeId( int scope ) { } } +void StackModel::setProjectPath( const std::string& projectPath ) { + std::string normalizedProjectPath( projectPath ); + FileSystem::dirAddSlashAtEnd( normalizedProjectPath ); + { + Lock l( mResourceLock ); + if ( mProjectPath == normalizedProjectPath ) + return; + mProjectPath = std::move( normalizedProjectPath ); + } + invalidate(); +} + +const char* StackModel::displaySourcePath( const std::string& sourcePath ) const { + if ( mProjectPath.empty() || sourcePath.size() < mProjectPath.size() || + 0 != sourcePath.compare( 0, mProjectPath.size(), mProjectPath ) ) + return sourcePath.c_str(); + return sourcePath.c_str() + mProjectPath.size(); +} + } // namespace ecode diff --git a/src/tools/ecode/plugins/debugger/models/stackmodel.hpp b/src/tools/ecode/plugins/debugger/models/stackmodel.hpp index e8f98246f..305a73f04 100644 --- a/src/tools/ecode/plugins/debugger/models/stackmodel.hpp +++ b/src/tools/ecode/plugins/debugger/models/stackmodel.hpp @@ -19,7 +19,7 @@ class StackModel : public Model { public: enum Columns { ID, Name, SourceName, SourcePath, Line, Column }; - StackModel( StackTraceInfo&& stack, UISceneNode* sceneNode ); + StackModel( StackTraceInfo&& stack, UISceneNode* sceneNode, std::string projectPath = {} ); virtual size_t rowCount( const ModelIndex& ) const; @@ -37,10 +37,15 @@ class StackModel : public Model { void setCurrentScopeId( int scope ); + void setProjectPath( const std::string& projectPath ); + protected: StackTraceInfo mStack; + std::string mProjectPath; UISceneNode* mSceneNode{ nullptr }; int mCurrentScopeId{ 0 }; + + const char* displaySourcePath( const std::string& sourcePath ) const; }; } // namespace ecode diff --git a/src/tools/ecode/plugins/debugger/statusdebuggercontroller.cpp b/src/tools/ecode/plugins/debugger/statusdebuggercontroller.cpp index b5bcc91a4..bf2f3d40e 100644 --- a/src/tools/ecode/plugins/debugger/statusdebuggercontroller.cpp +++ b/src/tools/ecode/plugins/debugger/statusdebuggercontroller.cpp @@ -1,11 +1,45 @@ #include "statusdebuggercontroller.hpp" +#include "../../appconfig.hpp" +#include "../../uirightpanel.hpp" #include "../../widgetcommandexecuter.hpp" #include "../plugincontextprovider.hpp" #include "eepp/ui/uiwidgetcreator.hpp" #include +#include namespace ecode { +static constexpr size_t DEBUGGER_LAYOUT_MAX_DEPTH = 16; +static constexpr size_t DEBUGGER_LAYOUT_MAX_NODES = 64; +static constexpr size_t DEBUGGER_WIDGET_ID_PREFIX_LENGTH = 9; + +static bool isValidDebuggerLayoutNode( const nlohmann::json& node, + UnorderedSet& widgetTypes, size_t depth, + size_t& nodeCount ) { + if ( depth > DEBUGGER_LAYOUT_MAX_DEPTH || ++nodeCount > DEBUGGER_LAYOUT_MAX_NODES ) + return false; + if ( !node.is_object() || !node.contains( "type" ) || !node["type"].is_string() ) + return false; + if ( node["type"] == "splitter" ) + return node.contains( "first" ) && node.contains( "last" ) && + isValidDebuggerLayoutNode( node["first"], widgetTypes, depth + 1, nodeCount ) && + isValidDebuggerLayoutNode( node["last"], widgetTypes, depth + 1, nodeCount ); + if ( node["type"] != "tabwidget" || !node.contains( "files" ) || !node["files"].is_array() ) + return false; + for ( const auto& file : node["files"] ) { + if ( !file.is_object() || !file.contains( "type" ) || !file["type"].is_string() || + !widgetTypes.emplace( file["type"].get() ).second ) + return false; + } + return true; +} + +static void restoreColumnWidths( UIAbstractTableView* view, const nlohmann::json& columns, + const char* key ) { + if ( view && columns.contains( key ) ) + view->unserializeColumnWidths( columns[key] ); +} + class UIBreakpointsTableCell : public UITableCell { public: static UIBreakpointsTableCell* New( const std::string& tag, const BreakpointsModel* model, @@ -109,6 +143,37 @@ StatusDebuggerController::StatusDebuggerController( UISplitter* mainSplitter, PluginContextProvider* pluginContext ) : StatusBarElement( mainSplitter, uiSceneNode, pluginContext ) {} +StatusDebuggerController::~StatusDebuggerController() { + mEventConnections.clear(); + mTabWidgetEventConnections.clear(); + if ( mContainer ) + mContainer->removeActionsByTag( reinterpret_cast( this ) ); + if ( mTabWidgetSplitter ) { + mTabWidgetSplitter->setOnTabWidgetCreateCb( nullptr ); + mTabWidgetSplitter->setOnTabWidgetCloseCb( nullptr ); + mTabWidgetSplitter->forEachTabWidget( []( UITabWidget* tabWidget ) { + tabWidget->setSplitFunction( nullptr ); + tabWidget->setTabTryCloseCallback( nullptr ); + tabWidget->setAcceptsDropOfWidgetFn( nullptr ); + } ); + eeDelete( mTabWidgetSplitter ); + } + if ( mContext && mContext->getRightPanel() ) + mContext->getRightPanel()->unregisterPanel( "debugger" ); + if ( mContainer ) + mContainer->close(); +} + +void StatusDebuggerController::show() { + StatusBarElement::show(); + updateRightPanel(); +} + +void StatusDebuggerController::hide() { + StatusBarElement::hide(); + updateRightPanel(); +} + UIWidget* StatusDebuggerController::getWidget() { return mContainer; } @@ -193,6 +258,207 @@ void StatusDebuggerController::clearConsoleBuffer() { } ); } +const std::string& StatusDebuggerController::saveLayout() { + saveTabLayout(); + return mSerializedLayout; +} + +void StatusDebuggerController::onTabCreated( UITab* tab, UIWidget* widget ) { + tab->setId( "debugger_tab_" + widget->getId().substr( DEBUGGER_WIDGET_ID_PREFIX_LENGTH ) ); + tab->addClass( "debugger-tab" ); + tab->setTextAsFallback( true ); + mEventConnections += tab->connect( Event::OnDragStart, + [this]( const Event* ) { beginRightPanelDropPreview(); } ); + mEventConnections += + tab->connect( Event::OnDragStop, [this]( const Event* ) { endRightPanelDropPreview(); } ); +} + +void StatusDebuggerController::onWidgetFocusChange( UIWidget* ) {} + +void StatusDebuggerController::createTabWidgets() { + mRightPanelContainer = mContext->getRightPanel() + ? mContext->getRightPanel()->registerPanel( "debugger" ) + : nullptr; + if ( !mDebuggerTabsContainer || !mRightPanelContainer ) + return; + + mTabWidgetSplitter = UITabWidgetSplitter::New( this, mUISceneNode ); + mTabWidgetSplitter->setHideTabBarOnSingleTab( false ); + mTabWidgetSplitter->setOnTabWidgetCreateCb( [this]( UITabWidget* tabWidget ) { + tabWidget->setTabsClosable( false ); + tabWidget->setAcceptsDropOfWidgetFn( + []( const UIWidget* widget ) { return widget->hasClass( "debugger-tab" ); } ); + tabWidget->setSplitFunction( + [this]( SplitDirection direction, UITabWidget* target ) -> UITabWidget* { + Node* dragging = mUISceneNode->getEventDispatcher()->getNodeDragging(); + if ( dragging && dragging->isType( UI_TYPE_TAB ) ) { + auto source = dragging->asType()->getTabWidget(); + bool sourceInRight = source && mRightPanelContainer->isParentOf( source ); + bool targetInRight = mRightPanelContainer->isParentOf( target ); + if ( source && sourceInRight != targetInRight ) + return target; + } + return mTabWidgetSplitter->splitTabWidget( direction, target ); + }, + mTabWidgetSplitter->getVisualSplitEdgePercent() ); + auto& connections = mTabWidgetEventConnections[tabWidget]; + connections += tabWidget->connect( Event::OnTabAdded, [this]( const Event* ) { + if ( !mRestoringLayout ) { + saveTabLayout(); + updateRightPanel(); + } + } ); + } ); + mTabWidgetSplitter->setOnTabWidgetCloseCb( + [this]( UITabWidget* tabWidget ) { mTabWidgetEventConnections.erase( tabWidget ); } ); + + mUITabWidget = mTabWidgetSplitter->createTabWidget( mDebuggerTabsContainer ); + mUITabWidget->setId( "app_debugger_tab_widget" ); + mUIRightTabWidget = mTabWidgetSplitter->createTabWidget( mRightPanelContainer ); + mUIRightTabWidget->setId( "app_debugger_right_tab_widget" ); + + const auto registerWidget = [this]( const std::string& type, UIWidget* widget, + const std::string& title ) { + widget->addClass( type ); + mTabWidgetSplitter->registerWidgetType( + type, { []( UIWidget* ) { return nlohmann::json::object(); }, + [widget, title]( const nlohmann::json& ) -> WidgetLoadResult { + return { widget, nullptr, title }; + } } ); + }; + registerWidget( "debugger-threads-and-stack", mUIThreadsSplitter, + mContext->i18n( "threads_and_stack", "Threads & Stack" ).toUtf8() ); + registerWidget( "debugger-variables", mUIVariables, + mContext->i18n( "variables", "Variables" ).toUtf8() ); + registerWidget( "debugger-expressions", mUIExpressions, + mContext->i18n( "expressions", "Expressions" ).toUtf8() ); + registerWidget( "debugger-breakpoints", mUIBreakpoints, + mContext->i18n( "breakpoints", "Breakpoints" ).toUtf8() ); + registerWidget( "debugger-console", mUIConsole, + mContext->i18n( "console_output", "Console Output" ).toUtf8() ); + + restoreTabLayout(); +} + +void StatusDebuggerController::restoreTabLayout() { + static const UnorderedSet expectedWidgetTypes{ + "debugger-threads-and-stack", "debugger-variables", "debugger-expressions", + "debugger-breakpoints", "debugger-console" }; + mRestoringLayout = true; + bool restored = false; + mSerializedLayout = mContext->getConfig().iniState.getValue( + "debugger", "panel_layout", + mContext->getConfig().iniState.getValue( "ui", "debugger_panel_layout", "" ) ); + const std::string& saved = mSerializedLayout; + if ( !saved.empty() ) { + auto layout = nlohmann::json::parse( saved, nullptr, false, true ); + UnorderedSet widgetTypes; + size_t nodeCount = 0; + const int version = layout.is_discarded() ? 0 : layout.value( "version", 0 ); + if ( !layout.is_discarded() && ( version == 2 || version == 3 ) && + layout.contains( "bottom" ) && layout.contains( "right" ) && + isValidDebuggerLayoutNode( layout["bottom"], widgetTypes, 0, nodeCount ) && + isValidDebuggerLayoutNode( layout["right"], widgetTypes, 0, nodeCount ) && + widgetTypes == expectedWidgetTypes ) { + mTabWidgetSplitter->unserializeNode( layout["bottom"], mUITabWidget ); + mTabWidgetSplitter->unserializeNode( layout["right"], mUIRightTabWidget ); + if ( layout.contains( "columns" ) && layout["columns"].is_object() ) { + restoreColumnWidths( mUIThreads, layout["columns"], "threads" ); + restoreColumnWidths( mUIStack, layout["columns"], "stack" ); + restoreColumnWidths( mUIVariables, layout["columns"], "variables" ); + restoreColumnWidths( mUIExpressions, layout["columns"], "expressions" ); + restoreColumnWidths( mUIBreakpoints, layout["columns"], "breakpoints" ); + } + restored = true; + } + } + + if ( !restored ) { + mTabWidgetSplitter->createWidgetInTabWidget( + mUITabWidget, mUIThreadsSplitter, + mContext->i18n( "threads_and_stack", "Threads & Stack" ).toUtf8(), false ); + mTabWidgetSplitter->createWidgetInTabWidget( + mUITabWidget, mUIBreakpoints, mContext->i18n( "breakpoints", "Breakpoints" ).toUtf8(), + false ); + mTabWidgetSplitter->createWidgetInTabWidget( + mUITabWidget, mUIConsole, mContext->i18n( "console_output", "Console Output" ).toUtf8(), + false ); + mTabWidgetSplitter->createWidgetInTabWidget( + mUIRightTabWidget, mUIVariables, mContext->i18n( "variables", "Variables" ).toUtf8(), + false ); + mTabWidgetSplitter->createWidgetInTabWidget( + mUIRightTabWidget, mUIExpressions, + mContext->i18n( "expressions", "Expressions" ).toUtf8(), false ); + mUITabWidget->setTabSelected( Uint32{ 0 } ); + mUIRightTabWidget->setTabSelected( Uint32{ 0 } ); + } + if ( mUITabWidget->getTabCount() ) + mTabWidgetSplitter->setCurrentWidget( + mUITabWidget->getTabSelected()->getOwnedWidget()->asType() ); + mRestoringLayout = false; + saveTabLayout(); + updateRightPanel(); +} + +void StatusDebuggerController::saveTabLayout() { + if ( mRestoringLayout || !mTabWidgetSplitter || !mDebuggerTabsContainer || + !mRightPanelContainer || !mDebuggerTabsContainer->getFirstChild() || + !mRightPanelContainer->getFirstChild() ) + return; + nlohmann::json layout; + layout["version"] = 3; + layout["bottom"] = mTabWidgetSplitter->serializeNode( mDebuggerTabsContainer->getFirstChild() ); + layout["right"] = mTabWidgetSplitter->serializeNode( mRightPanelContainer->getFirstChild() ); + layout["columns"]["threads"] = mUIThreads->serializeColumnWidths(); + layout["columns"]["stack"] = mUIStack->serializeColumnWidths(); + layout["columns"]["variables"] = mUIVariables->serializeColumnWidths(); + layout["columns"]["expressions"] = mUIExpressions->serializeColumnWidths(); + layout["columns"]["breakpoints"] = mUIBreakpoints->serializeColumnWidths(); + mSerializedLayout = layout.dump(); +} + +bool StatusDebuggerController::rightPanelHasTabs() const { + bool hasTabs = false; + if ( !mTabWidgetSplitter || !mRightPanelContainer ) + return false; + mTabWidgetSplitter->forEachTabWidgetStoppable( [this, &hasTabs]( UITabWidget* tabWidget ) { + hasTabs = mRightPanelContainer->isParentOf( tabWidget ) && tabWidget->getTabCount() > 0; + return hasTabs; + } ); + return hasTabs; +} + +void StatusDebuggerController::beginRightPanelDropPreview() { + if ( !mRightPanelDropPreview && mContainer && mContainer->isVisible() && + !rightPanelHasTabs() ) { + mRightPanelDropPreview = true; + updateRightPanel(); + } +} + +void StatusDebuggerController::endRightPanelDropPreview() { + if ( !mRightPanelDropPreview || !mContainer ) + return; + // The drop target is resolved after OnDragStop. Keep the empty panel alive until the next + // update so it can receive the drop, then collapse it if the drag was canceled or rejected. + mContainer->removeActionsByTag( reinterpret_cast( this ) ); + mContainer->runOnMainThread( + [this] { + mRightPanelDropPreview = false; + updateRightPanel(); + }, + Time::Zero, reinterpret_cast( this ) ); +} + +void StatusDebuggerController::updateRightPanel() { + auto rightPanel = mContext->getRightPanel(); + if ( !rightPanel || !mRightPanelContainer || !mTabWidgetSplitter ) + return; + rightPanel->setPanelVisible( "debugger", + mContainer && mContainer->isVisible() && + ( mRightPanelDropPreview || rightPanelHasTabs() ) ); +} + void StatusDebuggerController::createContainer() { if ( mContainer ) return; @@ -206,21 +472,16 @@ void StatusDebuggerController::createContainer() { } - + - - - - - - + + + + + + - - - - - - + @@ -248,7 +509,7 @@ void StatusDebuggerController::createContainer() { mContext->getStatusBar()->registerStatusBarPanel( mContainer, mContainer ); - mContainer->bind( "app_debugger_tab_widget", mUITabWidget ); + mContainer->bind( "app_debugger_tabs", mDebuggerTabsContainer ); mContainer->bind( "debugger_threads_and_stack", mUIThreadsSplitter ); mContainer->bind( "debugger_threads", mUIThreads ); mContainer->bind( "debugger_stack", mUIStack ); @@ -256,6 +517,8 @@ void StatusDebuggerController::createContainer() { mContainer->bind( "debugger_variables", mUIVariables ); mContainer->bind( "debugger_expressions", mUIExpressions ); mContainer->bind( "debugger_console", mUIConsole ); + mEventConnections += mContainer->connect( Event::OnVisibleChange, + [this]( const Event* ) { updateRightPanel(); } ); mContainer->bind( "app_debugger_start", mUIButStart ); mContainer->bind( "app_debugger_stop", mUIButStop ); mContainer->bind( "app_debugger_pause", mUIButPause ); @@ -264,25 +527,34 @@ void StatusDebuggerController::createContainer() { mContainer->bind( "app_debugger_step_into", mUIButStepInto ); mContainer->bind( "app_debugger_step_out", mUIButStepOut ); + createTabWidgets(); + mContainer->setCommand( "next-tab", [this] { - if ( mUITabWidget ) - mUITabWidget->focusNextTab(); + if ( mTabWidgetSplitter && mTabWidgetSplitter->getCurWidget() ) { + auto tabWidget = + mTabWidgetSplitter->tabWidgetFromWidget( mTabWidgetSplitter->getCurWidget() ); + if ( tabWidget ) + tabWidget->focusNextTab(); + } } ); mContainer->setCommand( "previous-tab", [this] { - if ( mUITabWidget ) - mUITabWidget->focusPreviousTab(); + if ( mTabWidgetSplitter && mTabWidgetSplitter->getCurWidget() ) { + auto tabWidget = + mTabWidgetSplitter->tabWidgetFromWidget( mTabWidgetSplitter->getCurWidget() ); + if ( tabWidget ) + tabWidget->focusPreviousTab(); + } } ); for ( int i = 1; i <= 5; i++ ) { mContainer->setCommand( String::format( "switch-to-tab-%d", i ), [this, i] { - if ( mUITabWidget ) - mUITabWidget->setTabSelected( - eeclamp( i - 1, 0, mUITabWidget->getTabCount() - 1 ) ); + if ( mTabWidgetSplitter ) + mTabWidgetSplitter->switchToTab( i - 1 ); } ); } - mContainer->on( Event::KeyDown, [this]( const Event* event ) { + mEventConnections += mContainer->connect( Event::KeyDown, [this]( const Event* event ) { auto ke = event->asKeyEvent(); if ( ke->getSanitizedMod() == 0 && ke->getKeyCode() == EE::Window::KEY_ESCAPE && mSplitter->getCurEditor() ) { @@ -313,19 +585,12 @@ void StatusDebuggerController::createContainer() { mUIThreads->setAutoExpandOnSingleColumn( true ); - mUIStack->setAutoColumnsWidth( true ); - mUIStack->setFitAllColumnsToWidget( true ); mUIStack->setMainColumn( 1 ); - mUIVariables->setAutoColumnsWidth( true ); - mUIVariables->setFitAllColumnsToWidget( true ); + mUIVariables->setMainColumn( 1 ); - mUIBreakpoints->setAutoColumnsWidth( true ); - mUIBreakpoints->setFitAllColumnsToWidget( true ); mUIBreakpoints->setMainColumn( 1 ); - mUIExpressions->setAutoColumnsWidth( true ); - mUIExpressions->setFitAllColumnsToWidget( true ); mUIExpressions->setMainColumn( 1 ); mUIConsole->setLocked( true ); @@ -333,7 +598,7 @@ void StatusDebuggerController::createContainer() { mUIConsole->setShowLineNumber( false ); mUIConsole->getDocument().reset(); mUIConsole->setScrollY( mUIConsole->getMaxScroll().y ); - mUIConsole->on( Event::OnScrollChange, [this]( auto ) { + mEventConnections += mUIConsole->connect( Event::OnScrollChange, [this]( const Event* ) { mScrollLocked = mUIConsole->getMaxScroll().y == mUIConsole->getScroll().y; } ); } diff --git a/src/tools/ecode/plugins/debugger/statusdebuggercontroller.hpp b/src/tools/ecode/plugins/debugger/statusdebuggercontroller.hpp index 857db1a4a..3b6f6fcce 100644 --- a/src/tools/ecode/plugins/debugger/statusdebuggercontroller.hpp +++ b/src/tools/ecode/plugins/debugger/statusdebuggercontroller.hpp @@ -3,8 +3,11 @@ #include "../../uistatusbar.hpp" #include "models/breakpointsmodel.hpp" +#include +#include #include #include +#include #include #include #include @@ -34,7 +37,7 @@ class UIBreakpointsTableView : public UITableView { UIWidget* createCell( UIWidget* rowWidget, const ModelIndex& index ); }; -class StatusDebuggerController : public StatusBarElement { +class StatusDebuggerController : public StatusBarElement, public UITabWidgetSplitter::Client { public: enum class State { NotStarted, Running, Paused }; @@ -43,7 +46,11 @@ class StatusDebuggerController : public StatusBarElement { StatusDebuggerController( UISplitter* mainSplitter, UISceneNode* uiSceneNode, PluginContextProvider* pluginContext ); - virtual ~StatusDebuggerController() {}; + virtual ~StatusDebuggerController(); + + virtual void show(); + + virtual void hide(); UIWidget* getWidget(); @@ -71,8 +78,14 @@ class StatusDebuggerController : public StatusBarElement { void setDebuggingState( State state ); + const std::string& saveLayout(); + std::function onWidgetCreated{ nullptr }; + void onTabCreated( UITab* tab, UIWidget* widget ); + + void onWidgetFocusChange( UIWidget* widget ); + protected: UIHLinearLayoutCommandExecuter* mContainer{ nullptr }; UITableView* mUIThreads{ nullptr }; @@ -90,9 +103,32 @@ class StatusDebuggerController : public StatusBarElement { UIPushButton* mUIButStepOver{ nullptr }; UIPushButton* mUIButStepOut{ nullptr }; UITabWidget* mUITabWidget{ nullptr }; + UITabWidget* mUIRightTabWidget{ nullptr }; + UITabWidgetSplitter* mTabWidgetSplitter{ nullptr }; + UILayout* mDebuggerTabsContainer{ nullptr }; + UILayout* mRightPanelContainer{ nullptr }; + Scene::EventConnectionList mEventConnections; + UnorderedMap mTabWidgetEventConnections; bool mScrollLocked{ true }; + bool mRestoringLayout{ false }; + bool mRightPanelDropPreview{ false }; + std::string mSerializedLayout; void createContainer(); + + void createTabWidgets(); + + void restoreTabLayout(); + + void saveTabLayout(); + + bool rightPanelHasTabs() const; + + void beginRightPanelDropPreview(); + + void endRightPanelDropPreview(); + + void updateRightPanel(); }; } // namespace ecode diff --git a/src/tools/ecode/plugins/plugincontextprovider.hpp b/src/tools/ecode/plugins/plugincontextprovider.hpp index dfd1a9863..3b4608097 100644 --- a/src/tools/ecode/plugins/plugincontextprovider.hpp +++ b/src/tools/ecode/plugins/plugincontextprovider.hpp @@ -19,6 +19,7 @@ class Font; namespace UI { class UISplitter; +class UILayout; class UITabWidget; class UISceneNode; class UICodeEditor; @@ -58,6 +59,8 @@ class ProjectDirectoryTree; struct TerminalConfig; class UIMainLayout; class UITreeViewFS; +class StatusDebuggerController; +class UIRightPanel; class PluginContextProvider { public: @@ -65,6 +68,10 @@ class PluginContextProvider { virtual UISplitter* getMainSplitter() const = 0; + virtual UIRightPanel* getRightPanel() const = 0; + + virtual StatusDebuggerController* getStatusDebuggerController() const = 0; + virtual UITreeViewFS* getProjectTreeView() const = 0; virtual void hideGlobalSearchBar() = 0; diff --git a/src/tools/ecode/uirightpanel.cpp b/src/tools/ecode/uirightpanel.cpp new file mode 100644 index 000000000..35b288134 --- /dev/null +++ b/src/tools/ecode/uirightpanel.cpp @@ -0,0 +1,101 @@ +#include "uirightpanel.hpp" +#include "appconfig.hpp" + +namespace ecode { + +using namespace EE; +using namespace EE::UI; + +UIRightPanel::UIRightPanel( UISplitter* splitter, UILayout* container, AppConfig* config ) : + mSplitter( splitter ), mContainer( container ), mConfig( config ) {} + +UILayout* UIRightPanel::registerPanel( const std::string& id ) { + auto found = mPanels.find( id ); + if ( found != mPanels.end() ) + return found->second.layout; + if ( !mContainer ) + return nullptr; + auto layout = UIRelativeLayout::New(); + layout->setId( id ); + layout->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); + layout->setVisible( false )->setEnabled( false )->setParent( mContainer ); + mPanels.emplace( id, Panel{ layout, false } ); + return layout; +} + +void UIRightPanel::unregisterPanel( const std::string& id ) { + auto found = mPanels.find( id ); + if ( found == mPanels.end() ) + return; + if ( found->second.layout ) + found->second.layout->close(); + mPanels.erase( found ); + updateVisibility(); +} + +void UIRightPanel::setPanelVisible( const std::string& id, bool visible ) { + auto found = mPanels.find( id ); + if ( found == mPanels.end() ) + return; + if ( visible ) { + for ( auto& panel : mPanels ) { + if ( panel.first != id && panel.second.visible ) { + panel.second.visible = false; + if ( panel.second.layout ) + panel.second.layout->setVisible( false )->setEnabled( false ); + } + } + } + if ( found->second.visible == visible ) + return; + found->second.visible = visible; + if ( found->second.layout ) + found->second.layout->setVisible( visible )->setEnabled( visible ); + updateVisibility(); +} + +bool UIRightPanel::isPanelVisible( const std::string& id ) const { + auto found = mPanels.find( id ); + return found != mPanels.end() && found->second.visible; +} + +void UIRightPanel::saveState() { + if ( mSplitter && mConfig && + mSplitter->getSplitPartition().getUnit() == StyleSheetLength::Percentage && + mSplitter->getSplitPartition().getValue() < 100.f ) + mConfig->windowState.rightPanelPartition = mSplitter->getSplitPartition().toString(); +} + +UISplitter* UIRightPanel::getSplitter() const { + return mSplitter; +} + +UILayout* UIRightPanel::getContainer() const { + return mContainer; +} + +void UIRightPanel::updateVisibility() { + if ( !mSplitter || !mConfig ) + return; + bool visible = false; + for ( const auto& panel : mPanels ) { + if ( panel.second.visible ) { + visible = true; + break; + } + } + if ( visible == mVisible ) + return; + mVisible = visible; + if ( visible ) { + auto partition = mConfig->windowState.rightPanelPartition; + if ( partition.empty() || partition == "100%" ) + partition = "75%"; + mSplitter->setSplitPartition( StyleSheetLength( partition ) ); + } else { + saveState(); + mSplitter->setSplitPartition( StyleSheetLength( "100%" ) ); + } +} + +} // namespace ecode diff --git a/src/tools/ecode/uirightpanel.hpp b/src/tools/ecode/uirightpanel.hpp new file mode 100644 index 000000000..f924b8e65 --- /dev/null +++ b/src/tools/ecode/uirightpanel.hpp @@ -0,0 +1,50 @@ +#ifndef ECODE_UIRIGHTPANEL_HPP +#define ECODE_UIRIGHTPANEL_HPP + +#include +#include +#include + +using namespace EE; +using namespace EE::UI; + +namespace ecode { + +class AppConfig; + +class UIRightPanel { + public: + UIRightPanel( UISplitter* splitter, UILayout* container, AppConfig* config ); + + UILayout* registerPanel( const std::string& id ); + + void unregisterPanel( const std::string& id ); + + void setPanelVisible( const std::string& id, bool visible ); + + bool isPanelVisible( const std::string& id ) const; + + void saveState(); + + UISplitter* getSplitter() const; + + UILayout* getContainer() const; + + protected: + struct Panel { + UILayout* layout{ nullptr }; + bool visible{ false }; + }; + + UISplitter* mSplitter{ nullptr }; + UILayout* mContainer{ nullptr }; + AppConfig* mConfig{ nullptr }; + UnorderedMap mPanels; + bool mVisible{ false }; + + void updateVisibility(); +}; + +} // namespace ecode + +#endif // ECODE_UIRIGHTPANEL_HPP