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
This commit is contained in:
Martín Lucas Golini
2026-08-14 00:17:29 -03:00
parent a2c9adca9a
commit bf750761ae
37 changed files with 1579 additions and 131 deletions
+11
View File
@@ -8,8 +8,19 @@ Your name is Negen (from negentropy: the process of creating order out of chaos)
1. **Performance & Memory Management:** 1. **Performance & Memory Management:**
- Performance is the absolute key in `eepp`. - Performance is the absolute key in `eepp`.
- Favor stack-allocated memory over heap allocations whenever possible. - 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. - Any heap allocation must be heavily justified.
- Exercise reason: maximize stack use for speed, but actively calculate boundaries to prevent stack-overflows. - 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: - 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. - 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. - Prefer move captures for owned temporary strings, buffers, vectors, and other heap-backed objects passed into lambdas.
+15
View File
@@ -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/`. * **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. * **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 ## C++ Virtual Method Style
Follow the convention already used by the class being edited. In particular, when a class declares 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 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 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 declarations. A class-wide conversion is a separate change and must update all applicable methods
together. 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.
+6 -4
View File
@@ -5,20 +5,22 @@ This project relies on a comprehensive suite of unit tests to prevent regression
## Running Tests ## Running Tests
The test binary manages its own current working directory, so you can execute it from anywhere. 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:** * **Default Execution for Agents on Linux & FreeBSD:**
Always run unit tests through the project wrapper unless the user explicitly asks for a different harness: 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:** * **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. 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:** * **Do not skip the wrapper for filtered tests:**
A focused test still needs the same wrapper: 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:** * **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: 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. 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):** * **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:** * **Filtering Tests:**
Use the `--filter` parameter to run specific tests (supports glob patterns). 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. Keep the wrapper in front of the binary unless the test is known not to create windows.
+2
View File
@@ -890,6 +890,8 @@ in der Baumansicht und in Öffen-/Schließdialogen aktivieren.</string>
<string name="uifiledialog_select_file">Eine Datei auswählen</string> <string name="uifiledialog_select_file">Eine Datei auswählen</string>
<string name="uifiledialog_select_folder">Einen Ordner auswählen</string> <string name="uifiledialog_select_folder">Einen Ordner auswählen</string>
<string name="uifiledialog_table">Tabelle</string> <string name="uifiledialog_table">Tabelle</string>
<string name="uitable_fit_columns_to_view">Spalten an Ansicht anpassen</string>
<string name="uitable_free_column_widths">Freie Spaltenbreiten</string>
<string name="uiterminal_copy">Kopieren</string> <string name="uiterminal_copy">Kopieren</string>
<string name="uiterminal_open_link">Link öffnen</string> <string name="uiterminal_open_link">Link öffnen</string>
<string name="uiterminal_paste">Einfügen</string> <string name="uiterminal_paste">Einfügen</string>
+2
View File
@@ -874,6 +874,8 @@ the directory tree and in file dialogs to open a folder or file.</string>
<string name="uifiledialog_select_file">Select a file</string> <string name="uifiledialog_select_file">Select a file</string>
<string name="uifiledialog_select_folder">Select a folder</string> <string name="uifiledialog_select_folder">Select a folder</string>
<string name="uifiledialog_table">Table</string> <string name="uifiledialog_table">Table</string>
<string name="uitable_fit_columns_to_view">Fit Columns to View</string>
<string name="uitable_free_column_widths">Free Column Widths</string>
<string name="uiterminal_copy">Copy</string> <string name="uiterminal_copy">Copy</string>
<string name="uiterminal_open_link">Open Link</string> <string name="uiterminal_open_link">Open Link</string>
<string name="uiterminal_paste">Paste</string> <string name="uiterminal_paste">Paste</string>
+2
View File
@@ -865,6 +865,8 @@ dans l'arborescence de répertoires ainsi que dans les boites de dialogues de s
<string name="uifiledialog_select_file">Sélectionner un fichier</string> <string name="uifiledialog_select_file">Sélectionner un fichier</string>
<string name="uifiledialog_select_folder">Sélectionner un dossier</string> <string name="uifiledialog_select_folder">Sélectionner un dossier</string>
<string name="uifiledialog_table">Table</string> <string name="uifiledialog_table">Table</string>
<string name="uitable_fit_columns_to_view">Ajuster les colonnes à la vue</string>
<string name="uitable_free_column_widths">Largeurs de colonnes libres</string>
<string name="uiterminal_copy">Copier</string> <string name="uiterminal_copy">Copier</string>
<string name="uiterminal_open_link">Ouvrir un lien</string> <string name="uiterminal_open_link">Ouvrir un lien</string>
<string name="uiterminal_paste">Coller</string> <string name="uiterminal_paste">Coller</string>
+2
View File
@@ -663,6 +663,8 @@ file in the directory tree.</string>
<string name="uifiledialog_select_file">选择文件</string> <string name="uifiledialog_select_file">选择文件</string>
<string name="uifiledialog_select_folder">选择文件夹</string> <string name="uifiledialog_select_folder">选择文件夹</string>
<string name="uifiledialog_table">Table</string> <string name="uifiledialog_table">Table</string>
<string name="uitable_fit_columns_to_view">列宽适应视图</string>
<string name="uitable_free_column_widths">自由调整列宽</string>
<string name="uiterminal_copy">复制</string> <string name="uiterminal_copy">复制</string>
<string name="uiterminal_open_link">打开链接</string> <string name="uiterminal_open_link">打开链接</string>
<string name="uiterminal_paste">粘贴</string> <string name="uiterminal_paste">粘贴</string>
+37 -2
View File
@@ -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 ### cursor
Read [cursor](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor) documentation. 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 ### splitter-always-show
Sets if the splitter divisor is always visible. If false it will be shown only if two views are Sets whether the splitter divisor remains visible whenever two views are attached. This property
attached to the splitter. overrides `splitter-hide-on-edge`.
* Applicable to: EE::UI::UISplitter (Splitter) * Applicable to: EE::UI::UISplitter (Splitter)
* Data Type: [boolean](#boolean-data-type) * 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 ### splitter-partition
Sets the space occupied by the first view contained by the splitter. Sets the space occupied by the first view contained by the splitter.
@@ -6,6 +6,7 @@
#include <eepp/ui/uitablecell.hpp> #include <eepp/ui/uitablecell.hpp>
#include <eepp/ui/uitableheadercolumn.hpp> #include <eepp/ui/uitableheadercolumn.hpp>
#include <eepp/ui/uitablerow.hpp> #include <eepp/ui/uitablerow.hpp>
#include <nlohmann/json_fwd.hpp>
#include <unordered_map> #include <unordered_map>
using namespace EE::Math; using namespace EE::Math;
@@ -20,6 +21,8 @@ namespace EE { namespace UI { namespace Abstract {
class EE_API UIAbstractTableView : public UIAbstractView { class EE_API UIAbstractTableView : public UIAbstractView {
public: public:
enum class ColumnWidthMode : Uint8 { Pixels, Percentage };
enum TableFlags : Uint32 { enum TableFlags : Uint32 {
TableFlagNone = 0, TableFlagNone = 0,
TableFlagHeaders = ( 1 << 0 ), TableFlagHeaders = ( 1 << 0 ),
@@ -85,6 +88,30 @@ class EE_API UIAbstractTableView : public UIAbstractView {
const Float& getColumnWidth( const size_t& colIndex ) const; const Float& getColumnWidth( const size_t& colIndex ) const;
Float getColumnWidthPercentage( const size_t& colIndex ) const;
std::vector<Float> 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<Float>& 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 ); virtual Float getMaxColumnContentWidth( const size_t& colIndex, bool bestGuess = false );
bool getAutoExpandOnSingleColumn() const; bool getAutoExpandOnSingleColumn() const;
@@ -180,6 +207,7 @@ class EE_API UIAbstractTableView : public UIAbstractView {
Float minHeight{ 0 }; Float minHeight{ 0 };
Float maxWidth{ 0 }; Float maxWidth{ 0 };
Float width{ 0 }; Float width{ 0 };
Float percentage{ 0 };
bool visible{ true }; bool visible{ true };
bool manuallySet{ false }; bool manuallySet{ false };
UIPushButton* widget{ nullptr }; UIPushButton* widget{ nullptr };
@@ -210,6 +238,10 @@ class EE_API UIAbstractTableView : public UIAbstractView {
std::function<void( UITableCell* )> mSetupCellCb; std::function<void( UITableCell* )> mSetupCellCb;
Float mRowHeaderWidth{ 0 }; Float mRowHeaderWidth{ 0 };
Uint32 mTableFlags{ UITABLE_DEFAULT_FLAGS }; Uint32 mTableFlags{ UITABLE_DEFAULT_FLAGS };
ColumnWidthMode mColumnWidthMode{ ColumnWidthMode::Pixels };
bool mColumnWidthModeMenuEnabled{ false };
bool mUpdatingColumnsForScrollbars{ false };
std::string mPendingSerializedColumnWidths;
virtual ~UIAbstractTableView(); virtual ~UIAbstractTableView();
@@ -231,6 +263,8 @@ class EE_API UIAbstractTableView : public UIAbstractView {
virtual void updateColumnsWidth(); virtual void updateColumnsWidth();
void restorePendingColumnWidths();
virtual Uint32 onFocus( NodeFocusReason reason ); virtual Uint32 onFocus( NodeFocusReason reason );
virtual Uint32 onFocusLoss(); virtual Uint32 onFocusLoss();
@@ -252,6 +286,8 @@ class EE_API UIAbstractTableView : public UIAbstractView {
virtual void onScrollChange(); virtual void onScrollChange();
virtual void onContentSizeChange();
virtual void onRowCreated( UITableRow* row ); virtual void onRowCreated( UITableRow* row );
virtual void onSortColumn( const size_t& colIndex ); virtual void onSortColumn( const size_t& colIndex );
@@ -270,6 +306,10 @@ class EE_API UIAbstractTableView : public UIAbstractView {
void resetColumnData(); void resetColumnData();
void updatePercentageColumnWidths();
int adjacentVisibleColumn( size_t column ) const;
void buildRowHeader(); void buildRowHeader();
void updateRowHeader( int realRowIndex, const ModelIndex& index, Float yOffset ); void updateRowHeader( int realRowIndex, const ModelIndex& index, Float yOffset );
+3
View File
@@ -232,6 +232,7 @@ enum class PropertyId : Uint16 {
TabAllowSwitchTabsInEmptySpaces, TabAllowSwitchTabsInEmptySpaces,
SplitterPartition, SplitterPartition,
SplitterAlwaysShow, SplitterAlwaysShow,
SplitterHideOnEdge,
DroppableHoveringColor, DroppableHoveringColor,
TextAsFallback, TextAsFallback,
SelectOnClick, SelectOnClick,
@@ -304,6 +305,8 @@ enum class PropertyId : Uint16 {
IconSize, IconSize,
SortIconSize, SortIconSize,
MainColumn, MainColumn,
ColumnWidthMode,
ColumnWidthModeMenu,
RowHeaderWidth, RowHeaderWidth,
TableFlags, TableFlags,
IndentWidth, IndentWidth,
@@ -1,6 +1,8 @@
#ifndef EE_UI_TOOLS_UITABWIDGETSPLITTER_HPP #ifndef EE_UI_TOOLS_UITABWIDGETSPLITTER_HPP
#define EE_UI_TOOLS_UITABWIDGETSPLITTER_HPP #define EE_UI_TOOLS_UITABWIDGETSPLITTER_HPP
#include <eepp/core/containers.hpp>
#include <eepp/scene/eventconnection.hpp>
#include <eepp/system/mutex.hpp> #include <eepp/system/mutex.hpp>
#include <eepp/ui/splitdirection.hpp> #include <eepp/ui/splitdirection.hpp>
#include <eepp/ui/uiscenenode.hpp> #include <eepp/ui/uiscenenode.hpp>
@@ -130,6 +132,8 @@ class EE_API UITabWidgetSplitter {
void setOnTabWidgetCreateCb( std::function<void( UITabWidget* )> cb ); void setOnTabWidgetCreateCb( std::function<void( UITabWidget* )> cb );
void setOnTabWidgetCloseCb( std::function<void( UITabWidget* )> cb );
void closeSplitter( UISplitter* splitter ); void closeSplitter( UISplitter* splitter );
void addRemainingTabWidgets( Node* widget ); void addRemainingTabWidgets( Node* widget );
@@ -254,10 +258,13 @@ class EE_API UITabWidgetSplitter {
Float mVisualSplitEdgePercent{ 0.1 }; Float mVisualSplitEdgePercent{ 0.1 };
Mutex mTabWidgetMutex; Mutex mTabWidgetMutex;
std::function<void( UITabWidget* )> mOnTabWidgetCreateCb; std::function<void( UITabWidget* )> mOnTabWidgetCreateCb;
std::function<void( UITabWidget* )> mOnTabWidgetCloseCb;
std::function<bool( SplitDirection direction, UIWidget* widget )> mCanCreateSplitFn; std::function<bool( SplitDirection direction, UIWidget* widget )> mCanCreateSplitFn;
TabTryCloseCallback mTabTryCloseCb; TabTryCloseCallback mTabTryCloseCb;
mutable Mutex mWidgetTypesMutex; mutable Mutex mWidgetTypesMutex;
std::unordered_map<std::string, WidgetTypeCallback> mWidgetTypes; UnorderedMap<std::string, WidgetTypeCallback> mWidgetTypes;
UnorderedMap<UITabWidget*, Scene::EventConnectionList> mTabWidgetEventConnections;
UnorderedMap<UIWidget*, Scene::EventConnectionList> mWidgetEventConnections;
UITabWidgetSplitter( Client* client, UISceneNode* sceneNode ); UITabWidgetSplitter( Client* client, UISceneNode* sceneNode );
+9
View File
@@ -1,6 +1,7 @@
#ifndef EE_UI_UISPLITTER_HPP #ifndef EE_UI_UISPLITTER_HPP
#define EE_UI_UISPLITTER_HPP #define EE_UI_UISPLITTER_HPP
#include <eepp/scene/eventconnection.hpp>
#include <eepp/ui/uilayout.hpp> #include <eepp/ui/uilayout.hpp>
namespace EE { namespace UI { namespace EE { namespace UI {
@@ -23,6 +24,10 @@ class EE_API UISplitter : public UILayout {
void setAlwaysShowSplitter( bool alwaysShowSplitter ); void setAlwaysShowSplitter( bool alwaysShowSplitter );
const bool& hideSplitterOnEdge() const;
void setHideSplitterOnEdge( bool hideSplitterOnEdge );
const StyleSheetLength& getSplitPartition() const; const StyleSheetLength& getSplitPartition() const;
void setSplitPartition( const StyleSheetLength& divisionSplit ); void setSplitPartition( const StyleSheetLength& divisionSplit );
@@ -49,10 +54,12 @@ class EE_API UISplitter : public UILayout {
protected: protected:
UIOrientation mOrientation; UIOrientation mOrientation;
bool mAlwaysShowSplitter; bool mAlwaysShowSplitter;
bool mHideSplitterOnEdge;
StyleSheetLength mSplitPartition; StyleSheetLength mSplitPartition;
UIWidget* mSplitter; UIWidget* mSplitter;
UIWidget* mFirstWidget; UIWidget* mFirstWidget;
UIWidget* mLastWidget; UIWidget* mLastWidget;
Scene::EventConnectionList mEventConnections;
UISplitter(); UISplitter();
@@ -63,6 +70,8 @@ class EE_API UISplitter : public UILayout {
void updateFromDrag(); void updateFromDrag();
void updateSplitterDragFlags(); void updateSplitterDragFlags();
bool shouldShowSplitter() const;
}; };
}} // namespace EE::UI }} // namespace EE::UI
+3 -1
View File
@@ -20,7 +20,7 @@ class EE_API UITableHeaderColumn : public UIPushButton {
protected: protected:
UIAbstractTableView* mView; UIAbstractTableView* mView;
size_t mColIndex; size_t mColIndex;
mutable UIImage* mImage{nullptr}; mutable UIImage* mImage{ nullptr };
Uint32 onCalculateDrag( const Vector2f& position, const Uint32& flags ); 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 onMouseClick( const Vector2i& position, const Uint32& flags );
Uint32 onMouseUp( const Vector2i& position, const Uint32& flags );
Uint32 onMouseDoubleClick( const Vector2i& position, const Uint32& flags ); Uint32 onMouseDoubleClick( const Vector2i& position, const Uint32& flags );
Uint32 onDragStop( const Vector2i& pos, const Uint32& flags ); Uint32 onDragStop( const Vector2i& pos, const Uint32& flags );
+2
View File
@@ -2,6 +2,7 @@
#define EE_UI_UITABWIDGET_HPP #define EE_UI_UITABWIDGET_HPP
#include <deque> #include <deque>
#include <eepp/scene/eventconnection.hpp>
#include <eepp/ui/splitdirection.hpp> #include <eepp/ui/splitdirection.hpp>
#include <eepp/ui/uitab.hpp> #include <eepp/ui/uitab.hpp>
#include <eepp/ui/uiwidget.hpp> #include <eepp/ui/uiwidget.hpp>
@@ -260,6 +261,7 @@ class EE_API UITabWidget : public UIWidget {
UIListView* mTabSwitcher{ nullptr }; UIListView* mTabSwitcher{ nullptr };
TabJumpMode mTabJumpMode{ TabJumpMode::Linear }; TabJumpMode mTabJumpMode{ TabJumpMode::Linear };
std::function<bool( const UIWidget* widget )> mAcceptsDropOfWidgetFn; std::function<bool( const UIWidget* widget )> mAcceptsDropOfWidgetFn;
Scene::EventConnectionList mEventConnections;
UITabWidget(); UITabWidget();
+295 -4
View File
@@ -9,6 +9,7 @@
#include <eepp/ui/uiscrollbar.hpp> #include <eepp/ui/uiscrollbar.hpp>
#include <eepp/window/engine.hpp> #include <eepp/window/engine.hpp>
#include <eepp/window/input.hpp> #include <eepp/window/input.hpp>
#include <nlohmann/json.hpp>
namespace EE { namespace UI { namespace Abstract { namespace EE { namespace UI { namespace Abstract {
@@ -104,6 +105,166 @@ const Float& UIAbstractTableView::getColumnWidth( const size_t& colIndex ) const
return columnData( colIndex ).width; return columnData( colIndex ).width;
} }
Float UIAbstractTableView::getColumnWidthPercentage( const size_t& colIndex ) const {
return columnData( colIndex ).percentage;
}
std::vector<Float> UIAbstractTableView::getColumnsWidthPercentage() const {
std::vector<Float> 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<Float>& 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<Float> 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<Float> values;
values.reserve( widths->size() );
for ( const auto& value : *widths ) {
if ( !value.is_number() )
return false;
values.emplace_back( value.get<Float>() );
}
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() { void UIAbstractTableView::selectAll() {
getSelection().clear(); getSelection().clear();
for ( size_t itemIndex = 0; itemIndex < getItemCount(); ++itemIndex ) { for ( size_t itemIndex = 0; itemIndex < getItemCount(); ++itemIndex ) {
@@ -139,11 +300,13 @@ void UIAbstractTableView::onModelUpdate( unsigned flags ) {
[this] { [this] {
modelUpdate( mPendingUpdateFlags.exchange( 0 ) ); modelUpdate( mPendingUpdateFlags.exchange( 0 ) );
createOrUpdateColumns( true ); createOrUpdateColumns( true );
restorePendingColumnWidths();
}, },
Time::Zero, onModelUpdateTag ); Time::Zero, onModelUpdateTag );
} else { } else {
UIAbstractView::onModelUpdate( flags ); UIAbstractView::onModelUpdate( flags );
createOrUpdateColumns( true ); createOrUpdateColumns( true );
restorePendingColumnWidths();
} }
} }
@@ -196,6 +359,9 @@ void UIAbstractTableView::createOrUpdateColumns( bool resetColumnData ) {
col.widget->setPixelsSize( col.width, getHeaderHeight() ); col.widget->setPixelsSize( col.width, getHeaderHeight() );
} }
if ( mColumnWidthMode == ColumnWidthMode::Percentage )
updatePercentageColumnWidths();
if ( mAutoColumnsWidth && visibleColCount > 1 ) { if ( mAutoColumnsWidth && visibleColCount > 1 ) {
Float contentWidth = getContentSpaceWidth(); Float contentWidth = getContentSpaceWidth();
bool shouldVScrollBeVisible = shouldVerticalScrollBeVisible(); bool shouldVScrollBeVisible = shouldVerticalScrollBeVisible();
@@ -311,9 +477,92 @@ void UIAbstractTableView::onSizeChange() {
createOrUpdateColumns( false ); createOrUpdateColumns( false );
} }
void UIAbstractTableView::onColumnSizeChange( const size_t&, bool fromUserInteraction ) { void UIAbstractTableView::onColumnSizeChange( const size_t& colIndex, bool fromUserInteraction ) {
if ( fromUserInteraction && mAutoColumnsWidth ) if ( fromUserInteraction && mAutoColumnsWidth )
mAutoColumnsWidth = false; 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 ) { 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 ) { void UIAbstractTableView::onColumnResizeToContent( const size_t& colIndex ) {
columnData( colIndex ).setWidth( getMaxColumnContentWidth( colIndex, true ) ); columnData( colIndex ).setWidth( getMaxColumnContentWidth( colIndex, true ) );
if ( mColumnWidthMode == ColumnWidthMode::Percentage ) {
onColumnSizeChange( colIndex, true );
return;
}
createOrUpdateColumns( false ); createOrUpdateColumns( false );
} }
@@ -582,6 +835,31 @@ void UIAbstractTableView::onScrollChange() {
mHeader->setPixelsPosition( mRowHeaderWidth + -mScrollOffset.x, 0 ); 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 ) { void UIAbstractTableView::bindNavigationClick( UIWidget* widget ) {
mWidgetsClickCbId[widget].push_back( mWidgetsClickCbId[widget].push_back(
widget->on( Event::MouseDoubleClick, [this]( const Event* event ) { widget->on( Event::MouseDoubleClick, [this]( const Event* event ) {
@@ -1012,6 +1290,14 @@ bool UIAbstractTableView::applyProperty( const StyleSheetProperty& attribute ) {
case PropertyId::MainColumn: case PropertyId::MainColumn:
setMainColumn( attribute.asInt() ); setMainColumn( attribute.asInt() );
break; 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: case PropertyId::RowHeaderWidth:
setRowHeaderWidth( setRowHeaderWidth(
lengthFromValue( attribute.getValue(), PropertyRelativeTarget::None ) ); lengthFromValue( attribute.getValue(), PropertyRelativeTarget::None ) );
@@ -1063,6 +1349,10 @@ std::string UIAbstractTableView::getPropertyString( const PropertyDefinition* pr
return String::fromFloat( (Float)getSortIconSize(), "px" ); return String::fromFloat( (Float)getSortIconSize(), "px" );
case PropertyId::MainColumn: case PropertyId::MainColumn:
return String::toString( (Int64)getMainColumn() ); return String::toString( (Int64)getMainColumn() );
case PropertyId::ColumnWidthMode:
return getColumnWidthMode() == ColumnWidthMode::Percentage ? "percentage" : "pixels";
case PropertyId::ColumnWidthModeMenu:
return isColumnWidthModeMenuEnabled() ? "true" : "false";
case PropertyId::RowHeaderWidth: case PropertyId::RowHeaderWidth:
return String::fromFloat( getRowHeaderWidth(), "px" ); return String::fromFloat( getRowHeaderWidth(), "px" );
case PropertyId::TableFlags: { case PropertyId::TableFlags: {
@@ -1099,9 +1389,10 @@ std::string UIAbstractTableView::getPropertyString( const PropertyDefinition* pr
std::vector<PropertyId> UIAbstractTableView::getPropertiesImplemented() const { std::vector<PropertyId> UIAbstractTableView::getPropertiesImplemented() const {
auto props = UIAbstractView::getPropertiesImplemented(); auto props = UIAbstractView::getPropertiesImplemented();
props.insert( props.end(), props.insert( props.end(), { PropertyId::RowHeight, PropertyId::IconSize,
{ PropertyId::RowHeight, PropertyId::IconSize, PropertyId::SortIconSize, PropertyId::SortIconSize, PropertyId::MainColumn,
PropertyId::MainColumn, PropertyId::RowHeaderWidth, PropertyId::TableFlags } ); PropertyId::ColumnWidthMode, PropertyId::ColumnWidthModeMenu,
PropertyId::RowHeaderWidth, PropertyId::TableFlags } );
return props; return props;
} }
@@ -336,6 +336,10 @@ void StyleSheetSpecification::registerDefaultProperties() {
.setType( PropertyType::NumberLength ); .setType( PropertyType::NumberLength );
registerProperty( PropertyId::MainColumn, "main-column", "0" ) registerProperty( PropertyId::MainColumn, "main-column", "0" )
.setType( PropertyType::NumberInt ); .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", "" ) registerProperty( PropertyId::RowHeaderWidth, "row-header-width", "" )
.setType( PropertyType::NumberLength ); .setType( PropertyType::NumberLength );
registerProperty( PropertyId::TableFlags, "table-flags", "" ) registerProperty( PropertyId::TableFlags, "table-flags", "" )
@@ -590,6 +594,8 @@ void StyleSheetSpecification::registerDefaultProperties() {
.setRelativeTarget( PropertyRelativeTarget::LocalBlockWidth ); .setRelativeTarget( PropertyRelativeTarget::LocalBlockWidth );
registerProperty( PropertyId::SplitterAlwaysShow, "splitter-always-show", "true" ) registerProperty( PropertyId::SplitterAlwaysShow, "splitter-always-show", "true" )
.setType( PropertyType::Bool ); .setType( PropertyType::Bool );
registerProperty( PropertyId::SplitterHideOnEdge, "splitter-hide-on-edge", "false" )
.setType( PropertyType::Bool );
registerProperty( PropertyId::DroppableHoveringColor, "droppable-hovering-color", "#FFFFFF20" ) registerProperty( PropertyId::DroppableHoveringColor, "droppable-hovering-color", "#FFFFFF20" )
.setType( PropertyType::Color ); .setType( PropertyType::Color );
@@ -1926,6 +1926,8 @@ void UICodeEditorSplitter::onTabClosed( const TabEvent* tabEvent ) {
Node* remainingNode = tabWidget == splitter->getFirstWidget() Node* remainingNode = tabWidget == splitter->getFirstWidget()
? splitter->getLastWidget() ? splitter->getLastWidget()
: splitter->getFirstWidget(); : splitter->getFirstWidget();
// Keep the surviving subtree out of closeSplitter()'s recursive bookkeeping.
remainingNode->detach();
closeSplitter( splitter ); closeSplitter( splitter );
eeASSERT( parent->getChildCount() == 0 ); eeASSERT( parent->getChildCount() == 0 );
remainingNode->setParent( parent ); remainingNode->setParent( parent );
+27 -8
View File
@@ -17,7 +17,13 @@ UITabWidgetSplitter::UITabWidgetSplitter( UITabWidgetSplitter::Client* client,
UISceneNode* sceneNode ) : UISceneNode* sceneNode ) :
mUISceneNode( sceneNode ), mClient( client ) {} mUISceneNode( sceneNode ), mClient( client ) {}
UITabWidgetSplitter::~UITabWidgetSplitter() {} UITabWidgetSplitter::~UITabWidgetSplitter() {
mTabWidgetEventConnections.clear();
mWidgetEventConnections.clear();
mTabWidgets.clear();
mCurWidget = nullptr;
mClient = nullptr;
}
UITabWidget* UITabWidgetSplitter::tabWidgetFromWidget( UIWidget* widget ) const { UITabWidget* UITabWidgetSplitter::tabWidgetFromWidget( UIWidget* widget ) const {
if ( widget ) if ( widget )
@@ -49,15 +55,15 @@ void UITabWidgetSplitter::setCurrentWidget( UIWidget* curWidget ) {
std::pair<UITab*, UIWidget*> std::pair<UITab*, UIWidget*>
UITabWidgetSplitter::createWidgetInTabWidget( UITabWidget* tabWidget, UIWidget* widget, UITabWidgetSplitter::createWidgetInTabWidget( UITabWidget* tabWidget, UIWidget* widget,
const std::string& tabName, bool focus ) { const std::string& tabName, bool focus ) {
eeASSERT( curWidgetExists() );
if ( nullptr == tabWidget ) if ( nullptr == tabWidget )
return std::make_pair( (UITab*)nullptr, (UIWidget*)nullptr ); return std::make_pair( (UITab*)nullptr, (UIWidget*)nullptr );
UITab* tab = tabWidget->add( tabName, widget ); UITab* tab = tabWidget->add( tabName, widget );
widget->setData( (UintPtr)tab ); 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<UIWidget>() ); setCurrentWidget( event->getNode()->asType<UIWidget>() );
} ); } );
widget->on( Event::OnTitleChange, [this]( const Event* event ) { connections += widget->connect( Event::OnTitleChange, [this]( const Event* event ) {
const TextEvent* tevent = static_cast<const TextEvent*>( event ); const TextEvent* tevent = static_cast<const TextEvent*>( event );
UIWidget* widget = event->getNode()->asType<UIWidget>(); UIWidget* widget = event->getNode()->asType<UIWidget>();
UITabWidget* tabWidget = tabWidgetFromWidget( widget ); UITabWidget* tabWidget = tabWidgetFromWidget( widget );
@@ -157,7 +163,8 @@ UITabWidget* UITabWidgetSplitter::createTabWidget( Node* parent ) {
}, },
mVisualSplitEdgePercent ); 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<UITabWidget>(); UITabWidget* tabWidget = event->getNode()->asType<UITabWidget>();
eeASSERT( nullptr != tabWidget && nullptr != tabWidget->getTabSelected() && eeASSERT( nullptr != tabWidget && nullptr != tabWidget->getTabSelected() &&
nullptr != tabWidget->getTabSelected()->getOwnedWidget() ); nullptr != tabWidget->getTabSelected()->getOwnedWidget() );
@@ -175,7 +182,7 @@ UITabWidget* UITabWidgetSplitter::createTabWidget( Node* parent ) {
} }
return false; return false;
} ); } );
tabWidget->on( Event::OnTabClosed, [this]( const Event* event ) { connections += tabWidget->connect( Event::OnTabClosed, [this]( const Event* event ) {
onTabClosed( static_cast<const TabEvent*>( event ) ); onTabClosed( static_cast<const TabEvent*>( event ) );
} ); } );
if ( mOnTabWidgetCreateCb ) if ( mOnTabWidgetCreateCb )
@@ -285,6 +292,7 @@ void UITabWidgetSplitter::closeTab( UIWidget* widget,
UITabWidget::FocusTabBehavior focusTabBehavior ) { UITabWidget::FocusTabBehavior focusTabBehavior ) {
if ( widget ) { if ( widget ) {
UITabWidget* tabWidget = tabWidgetFromWidget( widget ); UITabWidget* tabWidget = tabWidgetFromWidget( widget );
mWidgetEventConnections.erase( widget );
if ( tabWidget ) if ( tabWidget )
tabWidget->removeTab( (UITab*)widget->getData(), true, false, focusTabBehavior ); tabWidget->removeTab( (UITab*)widget->getData(), true, false, focusTabBehavior );
if ( mCurWidget == widget ) if ( mCurWidget == widget )
@@ -497,9 +505,12 @@ void UITabWidgetSplitter::closeTabWidgets( UISplitter* splitter ) {
Node* node = splitter->getFirstChild(); Node* node = splitter->getFirstChild();
while ( node ) { while ( node ) {
if ( node->isType( UI_TYPE_TABWIDGET ) ) { if ( node->isType( UI_TYPE_TABWIDGET ) ) {
auto it = auto tabWidget = node->asType<UITabWidget>();
std::find( mTabWidgets.begin(), mTabWidgets.end(), node->asType<UITabWidget>() ); auto it = std::find( mTabWidgets.begin(), mTabWidgets.end(), tabWidget );
if ( it != mTabWidgets.end() ) { if ( it != mTabWidgets.end() ) {
if ( mOnTabWidgetCloseCb )
mOnTabWidgetCloseCb( tabWidget );
mTabWidgetEventConnections.erase( tabWidget );
Lock l( mTabWidgetMutex ); Lock l( mTabWidgetMutex );
mTabWidgets.erase( it ); mTabWidgets.erase( it );
} }
@@ -622,6 +633,9 @@ void UITabWidgetSplitter::onTabClosed( const TabEvent* tabEvent ) {
if ( tabWidget->getTabCount() == 0 ) { if ( tabWidget->getTabCount() == 0 ) {
UISplitter* splitter = splitterFromWidget( widget ); UISplitter* splitter = splitterFromWidget( widget );
if ( splitter && splitter->isFull() ) { if ( splitter && splitter->isFull() ) {
if ( mOnTabWidgetCloseCb )
mOnTabWidgetCloseCb( tabWidget );
mTabWidgetEventConnections.erase( tabWidget );
tabWidget->close(); tabWidget->close();
auto itWidget = std::find( mTabWidgets.begin(), mTabWidgets.end(), tabWidget ); auto itWidget = std::find( mTabWidgets.begin(), mTabWidgets.end(), tabWidget );
if ( itWidget != mTabWidgets.end() ) { if ( itWidget != mTabWidgets.end() ) {
@@ -647,6 +661,7 @@ void UITabWidgetSplitter::onTabClosed( const TabEvent* tabEvent ) {
Node* remainingNode = tabWidget == splitter->getFirstWidget() Node* remainingNode = tabWidget == splitter->getFirstWidget()
? splitter->getLastWidget() ? splitter->getLastWidget()
: splitter->getFirstWidget(); : splitter->getFirstWidget();
remainingNode->detach();
closeSplitter( splitter ); closeSplitter( splitter );
eeASSERT( parent->getChildCount() == 0 ); eeASSERT( parent->getChildCount() == 0 );
remainingNode->setParent( parent ); remainingNode->setParent( parent );
@@ -676,6 +691,10 @@ void UITabWidgetSplitter::setOnTabWidgetCreateCb( std::function<void( UITabWidge
mOnTabWidgetCreateCb = std::move( cb ); mOnTabWidgetCreateCb = std::move( cb );
} }
void UITabWidgetSplitter::setOnTabWidgetCloseCb( std::function<void( UITabWidget* )> cb ) {
mOnTabWidgetCloseCb = std::move( cb );
}
bool UITabWidgetSplitter::getVisualSplitting() const { bool UITabWidgetSplitter::getVisualSplitting() const {
return mVisualSplitting; return mVisualSplitting;
} }
+40 -12
View File
@@ -12,29 +12,33 @@ UISplitter::UISplitter() :
UILayout( "splitter" ), UILayout( "splitter" ),
mOrientation( UIOrientation::Horizontal ), mOrientation( UIOrientation::Horizontal ),
mAlwaysShowSplitter( true ), mAlwaysShowSplitter( true ),
mHideSplitterOnEdge( false ),
mSplitPartition( StyleSheetLength( "50%" ) ), mSplitPartition( StyleSheetLength( "50%" ) ),
mFirstWidget( NULL ), mFirstWidget( NULL ),
mLastWidget( NULL ) { mLastWidget( NULL ) {
mFlags |= UI_OWNS_CHILDREN_POSITION; mFlags |= UI_OWNS_CHILDREN_POSITION;
mSplitter = UIWidget::NewWithTag( "splitter::separator" ); mSplitter = UIWidget::NewWithTag( "splitter::separator" );
mSplitter->setDragEnabled( true ); mSplitter->setDragEnabled( true );
mSplitter->on( Event::OnDragStart, mEventConnections += mSplitter->connect( Event::OnDragStart, [this]( const Event* ) {
[this]( const Event* ) { mSplitter->pushState( UIState::StateSelected ); } ); mSplitter->pushState( UIState::StateSelected );
mSplitter->on( Event::OnDragStop, } );
[this]( const Event* ) { mSplitter->popState( UIState::StateSelected ); } ); mEventConnections += mSplitter->connect( Event::OnDragStop, [this]( const Event* ) {
mSplitter->popState( UIState::StateSelected );
} );
mSplitter->setParent( this ); mSplitter->setParent( this );
mSplitter->setMinWidth( 4 ); mSplitter->setMinWidth( 4 );
mSplitter->setMinHeight( 4 ); mSplitter->setMinHeight( 4 );
mSplitter->on( Event::OnSizeChange, [this]( const Event* ) { mEventConnections += mSplitter->connect( Event::OnSizeChange, [this]( const Event* ) {
setLayoutDirty( LayoutInvalidation::ContainerLayout ); 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 getUISceneNode()->setCursor( mOrientation == UIOrientation::Horizontal ? Cursor::SizeWE
: Cursor::SizeNS ); : Cursor::SizeNS );
} ); } );
mSplitter->on( Event::MouseLeave, mEventConnections += mSplitter->connect( Event::MouseLeave, [this]( const Event* ) {
[this]( const Event* ) { getUISceneNode()->setCursor( Cursor::Arrow ); } ); getUISceneNode()->setCursor( Cursor::Arrow );
mSplitter->on( Event::OnPositionChange, [this]( const Event* ) { } );
mEventConnections += mSplitter->connect( Event::OnPositionChange, [this]( const Event* ) {
if ( mSplitter->isDragging() && !mDirtyLayout ) if ( mSplitter->isDragging() && !mDirtyLayout )
updateFromDrag(); 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 { const StyleSheetLength& UISplitter::getSplitPartition() const {
return mSplitPartition; return mSplitPartition;
} }
@@ -127,6 +142,9 @@ bool UISplitter::applyProperty( const StyleSheetProperty& attribute ) {
case PropertyId::SplitterAlwaysShow: case PropertyId::SplitterAlwaysShow:
setAlwaysShowSplitter( attribute.asBool() ); setAlwaysShowSplitter( attribute.asBool() );
break; break;
case PropertyId::SplitterHideOnEdge:
setHideSplitterOnEdge( attribute.asBool() );
break;
case PropertyId::Orientation: case PropertyId::Orientation:
setOrientation( String::iequals( attribute.getValue(), "horizontal" ) setOrientation( String::iequals( attribute.getValue(), "horizontal" )
? UIOrientation::Horizontal ? UIOrientation::Horizontal
@@ -149,6 +167,8 @@ std::string UISplitter::getPropertyString( const PropertyDefinition* propertyDef
return getSplitPartition().toString(); return getSplitPartition().toString();
case PropertyId::SplitterAlwaysShow: case PropertyId::SplitterAlwaysShow:
return alwaysShowSplitter() ? "true" : "false"; return alwaysShowSplitter() ? "true" : "false";
case PropertyId::SplitterHideOnEdge:
return hideSplitterOnEdge() ? "true" : "false";
case PropertyId::Orientation: case PropertyId::Orientation:
return getOrientation() == UIOrientation::Horizontal ? "horizontal" : "vertical"; return getOrientation() == UIOrientation::Horizontal ? "horizontal" : "vertical";
default: default:
@@ -159,7 +179,7 @@ std::string UISplitter::getPropertyString( const PropertyDefinition* propertyDef
std::vector<PropertyId> UISplitter::getPropertiesImplemented() const { std::vector<PropertyId> UISplitter::getPropertiesImplemented() const {
auto props = UIWidget::getPropertiesImplemented(); auto props = UIWidget::getPropertiesImplemented();
auto local = { PropertyId::SplitterPartition, PropertyId::SplitterAlwaysShow, auto local = { PropertyId::SplitterPartition, PropertyId::SplitterAlwaysShow,
PropertyId::Orientation }; PropertyId::SplitterHideOnEdge, PropertyId::Orientation };
props.insert( props.end(), local.begin(), local.end() ); props.insert( props.end(), local.begin(), local.end() );
return props; return props;
} }
@@ -204,7 +224,7 @@ void UISplitter::onChildCountChange( Node* child, const bool& removed ) {
void UISplitter::updateFromDrag() { void UISplitter::updateFromDrag() {
mDirtyLayout = true; mDirtyLayout = true;
mSplitter->setVisible( !mAlwaysShowSplitter && !mLastWidget ? false : true ); mSplitter->setVisible( shouldShowSplitter() );
mSplitter->setEnabled( mSplitter->isVisible() ); mSplitter->setEnabled( mSplitter->isVisible() );
if ( UIOrientation::Horizontal == mOrientation ) { if ( UIOrientation::Horizontal == mOrientation ) {
@@ -351,7 +371,7 @@ void UISplitter::updateLayout() {
return; return;
} }
mSplitter->setVisible( !mAlwaysShowSplitter && !mLastWidget ? false : true ); mSplitter->setVisible( shouldShowSplitter() );
mSplitter->setEnabled( mSplitter->isVisible() ); mSplitter->setEnabled( mSplitter->isVisible() );
Float totalSpace = mOrientation == UIOrientation::Horizontal Float totalSpace = mOrientation == UIOrientation::Horizontal
? mSize.getWidth() - mPaddingPx.Left - mPaddingPx.Right ? mSize.getWidth() - mPaddingPx.Left - mPaddingPx.Right
@@ -426,6 +446,14 @@ void UISplitter::updateSplitterDragFlags() {
: UI_DRAG_HORIZONTAL ); : 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 ) { Uint32 UISplitter::onMessage( const NodeMessage* Msg ) {
switch ( Msg->getMsg() ) { switch ( Msg->getMsg() ) {
case NodeMessage::LayoutAttributeChange: { case NodeMessage::LayoutAttributeChange: {
+27 -1
View File
@@ -1,4 +1,6 @@
#include <eepp/ui/abstract/uiabstracttableview.hpp> #include <eepp/ui/abstract/uiabstracttableview.hpp>
#include <eepp/ui/uimenuitem.hpp>
#include <eepp/ui/uipopupmenu.hpp>
#include <eepp/ui/uiscenenode.hpp> #include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uitableheadercolumn.hpp> #include <eepp/ui/uitableheadercolumn.hpp>
@@ -31,7 +33,7 @@ Uint32 UITableHeaderColumn::onCalculateDrag( const Vector2f& position, const Uin
} }
Vector2f pos( eefloor( position.x ), eefloor( position.y ) ); Vector2f pos( eefloor( position.x ), eefloor( position.y ) );
if ( mDragPoint != pos && std::abs( mDragPoint.x - pos.x ) > 1.f ) { 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 ) ) { if ( onDrag( pos, flags, dragDiff ) ) {
mDragPoint = pos; mDragPoint = pos;
eventDispatcher->setNodeDragging( this ); eventDispatcher->setNodeDragging( this );
@@ -77,6 +79,30 @@ Uint32 UITableHeaderColumn::onMouseClick( const Vector2i& position, const Uint32
return UIPushButton::onMouseClick( position, flags ); 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&, Uint32 UITableHeaderColumn::onDrag( const Vector2f& position, const Uint32&,
const Sizef& dragDiff ) { const Sizef& dragDiff ) {
Vector2f localPos( convertToNodeSpace( position ) ); Vector2f localPos( convertToNodeSpace( position ) );
+8 -2
View File
@@ -87,8 +87,10 @@ UITabWidget::UITabWidget() :
mTabScroll = UIScrollBar::NewHorizontalWithTag( "scrollbarmini" ); mTabScroll = UIScrollBar::NewHorizontalWithTag( "scrollbarmini" );
mTabScroll->setParent( mTabBar ); mTabScroll->setParent( mTabBar );
mTabScroll->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent ); mTabScroll->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent );
mTabScroll->on( Event::OnSizeChange, [this]( const Event* ) { updateScrollBar(); } ); mEventConnections +=
mTabScroll->on( Event::OnValueChange, [this]( const Event* ) { updateScroll(); } ); mTabScroll->connect( Event::OnSizeChange, [this]( const Event* ) { updateScrollBar(); } );
mEventConnections +=
mTabScroll->connect( Event::OnValueChange, [this]( const Event* ) { updateScroll(); } );
onSizeChange(); onSizeChange();
@@ -560,6 +562,10 @@ UITab* UITabWidget::add( const String& text, UINode* nodeOwned, DrawablePtr icon
UITabWidget* UITabWidget::add( UITab* tab ) { UITabWidget* UITabWidget::add( UITab* tab ) {
tab->setParent( mTabBar ); tab->setParent( mTabBar );
if ( tab->getOwnedWidget() && tab->getOwnedWidget()->isWidget() )
tab->getOwnedWidget()->asType<UIWidget>()->setLayoutSizePolicy( SizePolicy::Fixed,
SizePolicy::Fixed );
refreshOwnedWidget( tab );
mTabs.push_back( tab ); mTabs.push_back( tab );
@@ -3,6 +3,8 @@
#include <eepp/system/sys.hpp> #include <eepp/system/sys.hpp>
#include <eepp/ui/tools/uitabwidgetsplitter.hpp> #include <eepp/ui/tools/uitabwidgetsplitter.hpp>
#include <eepp/ui/uiapplication.hpp> #include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uiscrollbar.hpp>
#include <eepp/ui/uitableview.hpp>
#include <eepp/ui/uiwidget.hpp> #include <eepp/ui/uiwidget.hpp>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
@@ -21,6 +23,415 @@ class TestClient : public UITabWidgetSplitter::Client {
void onWidgetFocusChange( UIWidget* ) override { focusChangeCount++; } 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<ThreeColumnModel>() );
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<ThreeColumnModel>() );
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<ThreeColumnModel>() );
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<ThreeColumnModel>() );
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(<TableView />)xml" )->asType<UITableView>();
EXPECT_FALSE( defaultTable->isColumnWidthModeMenuEnabled() );
auto* table = app.getUI()
->loadLayoutFromString( R"xml(<TableView column-width-mode="percentage"
column-width-mode-menu="true" />)xml" )
->asType<UITableView>();
EXPECT_TRUE( table->getColumnWidthMode() == UIAbstractTableView::ColumnWidthMode::Percentage );
EXPECT_TRUE( table->isColumnWidthModeMenuEnabled() );
auto* cssRoot = app.getUI()->loadLayoutFromString( R"xml(
<vbox>
<style>#css_table { column-width-mode-menu: true; }</style>
<TableView id="css_table" />
</vbox>)xml" );
auto* cssTable = cssRoot->querySelector( "#css_table" )->asType<UITableView>();
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<ThreeColumnModel>() );
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<ThreeColumnModel>( 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<ThreeColumnModel>( 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<ThreeColumnModel>() );
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<ThreeColumnModel>() );
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 ) { UTEST( UITabWidgetSplitter, Serialization ) {
UIApplication app( UIApplication app(
WindowSettings( 800, 600, "eepp - unit tests" ), WindowSettings( 800, 600, "eepp - unit tests" ),
@@ -34,15 +445,14 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* tabWidget = splitter->createTabWidget( (Node*)app.getUI() ); auto* tabWidget = splitter->createTabWidget( (Node*)app.getUI() );
bool saved = false; bool saved = false;
splitter->registerWidgetType( splitter->registerWidgetType( "testwidget",
"testwidget", { [&]( UIWidget* ) {
{ [&]( UIWidget* ) { saved = true;
saved = true; return nlohmann::json{ { "custom_key", 42 } };
return nlohmann::json{ { "custom_key", 42 } }; },
}, []( const nlohmann::json& ) -> WidgetLoadResult {
[]( const nlohmann::json& ) -> WidgetLoadResult { return { nullptr, nullptr, "" };
return { nullptr, nullptr, "" }; } } );
} } );
auto* w = UIWidget::New(); auto* w = UIWidget::New();
w->addClass( "testwidget" ); w->addClass( "testwidget" );
@@ -93,9 +503,8 @@ UTEST( UITabWidgetSplitter, Serialization ) {
splitter->createWidgetInTabWidget( tabWidget, ghost, "Ghost" ); splitter->createWidgetInTabWidget( tabWidget, ghost, "Ghost" );
splitter->registerWidgetType( splitter->registerWidgetType(
"known", "known", { []( UIWidget* ) { return nlohmann::json{ { "val", 1 } }; },
{ []( UIWidget* ) { return nlohmann::json{ { "val", 1 } }; }, []( const nlohmann::json& ) -> WidgetLoadResult { return {}; } } );
[]( const nlohmann::json& ) -> WidgetLoadResult { return {}; } } );
auto* reg = UIWidget::New(); auto* reg = UIWidget::New();
reg->addClass( "known" ); reg->addClass( "known" );
@@ -117,13 +526,12 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* tabWidget = splitter->createTabWidget( (Node*)app.getUI() ); auto* tabWidget = splitter->createTabWidget( (Node*)app.getUI() );
splitter->registerWidgetType( splitter->registerWidgetType(
"panewidget", "panewidget", { []( UIWidget* ) { return nlohmann::json{ { "pane", true } }; },
{ []( UIWidget* ) { return nlohmann::json{ { "pane", true } }; }, []( const nlohmann::json& j ) -> WidgetLoadResult {
[]( const nlohmann::json& j ) -> WidgetLoadResult { auto* w = UIWidget::New();
auto* w = UIWidget::New(); w->addClass( "panewidget" );
w->addClass( "panewidget" ); return { w, nullptr, j.value( "title", "" ) };
return { w, nullptr, j.value( "title", "" ) }; } } );
} } );
auto* w1 = UIWidget::New(); auto* w1 = UIWidget::New();
w1->addClass( "panewidget" ); w1->addClass( "panewidget" );
@@ -163,13 +571,12 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* tab1 = splitter1->createTabWidget( (Node*)app.getUI() ); auto* tab1 = splitter1->createTabWidget( (Node*)app.getUI() );
splitter1->registerWidgetType( splitter1->registerWidgetType(
"mywidget", "mywidget", { []( UIWidget* ) { return nlohmann::json{ { "data", "hello" } }; },
{ []( UIWidget* ) { return nlohmann::json{ { "data", "hello" } }; }, []( const nlohmann::json& j ) -> WidgetLoadResult {
[]( const nlohmann::json& j ) -> WidgetLoadResult { auto* w = UIWidget::New();
auto* w = UIWidget::New(); w->addClass( "mywidget" );
w->addClass( "mywidget" ); return { w, nullptr, j.value( "title", "" ) };
return { w, nullptr, j.value( "title", "" ) }; } } );
} } );
auto* wa = UIWidget::New(); auto* wa = UIWidget::New();
wa->addClass( "mywidget" ); wa->addClass( "mywidget" );
@@ -186,14 +593,13 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* splitter2 = UITabWidgetSplitter::New( &client2, app.getUI() ); auto* splitter2 = UITabWidgetSplitter::New( &client2, app.getUI() );
auto* tab2 = splitter2->createTabWidget( (Node*)app.getUI() ); auto* tab2 = splitter2->createTabWidget( (Node*)app.getUI() );
splitter2->registerWidgetType( splitter2->registerWidgetType( "mywidget",
"mywidget", { []( UIWidget* ) { return nlohmann::json::object(); },
{ []( UIWidget* ) { return nlohmann::json::object(); }, []( const nlohmann::json& j ) -> WidgetLoadResult {
[]( const nlohmann::json& j ) -> WidgetLoadResult { auto* w = UIWidget::New();
auto* w = UIWidget::New(); w->addClass( "mywidget" );
w->addClass( "mywidget" ); return { w, nullptr, j.value( "title", "" ) };
return { w, nullptr, j.value( "title", "" ) }; } } );
} } );
splitter2->fromJSON( saved ); splitter2->fromJSON( saved );
@@ -256,14 +662,13 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* splitter1 = UITabWidgetSplitter::New( &client1, app.getUI() ); auto* splitter1 = UITabWidgetSplitter::New( &client1, app.getUI() );
auto* tab1 = splitter1->createTabWidget( (Node*)app.getUI() ); auto* tab1 = splitter1->createTabWidget( (Node*)app.getUI() );
splitter1->registerWidgetType( splitter1->registerWidgetType( "partwidget",
"partwidget", { []( UIWidget* ) { return nlohmann::json::object(); },
{ []( UIWidget* ) { return nlohmann::json::object(); }, []( const nlohmann::json& j ) -> WidgetLoadResult {
[]( const nlohmann::json& j ) -> WidgetLoadResult { auto* w = UIWidget::New();
auto* w = UIWidget::New(); w->addClass( "partwidget" );
w->addClass( "partwidget" ); return { w, nullptr, j.value( "title", "" ) };
return { w, nullptr, j.value( "title", "" ) }; } } );
} } );
auto* w1 = UIWidget::New(); auto* w1 = UIWidget::New();
w1->addClass( "partwidget" ); w1->addClass( "partwidget" );
@@ -287,14 +692,13 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* splitter2 = UITabWidgetSplitter::New( &client2, app.getUI() ); auto* splitter2 = UITabWidgetSplitter::New( &client2, app.getUI() );
splitter2->createTabWidget( (Node*)app.getUI() ); splitter2->createTabWidget( (Node*)app.getUI() );
splitter2->registerWidgetType( splitter2->registerWidgetType( "partwidget",
"partwidget", { []( UIWidget* ) { return nlohmann::json::object(); },
{ []( UIWidget* ) { return nlohmann::json::object(); }, []( const nlohmann::json& j ) -> WidgetLoadResult {
[]( const nlohmann::json& j ) -> WidgetLoadResult { auto* w = UIWidget::New();
auto* w = UIWidget::New(); w->addClass( "partwidget" );
w->addClass( "partwidget" ); return { w, nullptr, j.value( "title", "" ) };
return { w, nullptr, j.value( "title", "" ) }; } } );
} } );
splitter2->fromJSON( saved ); splitter2->fromJSON( saved );
+2
View File
@@ -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.winIcon = ini.getValue( "window", "winicon", resPath + "icon/ecode.png" );
windowState.panelPartition = iniState.getValue( "window", "panel_partition", "15%" ); windowState.panelPartition = iniState.getValue( "window", "panel_partition", "15%" );
windowState.statusBarPartition = iniState.getValue( "window", "status_bar_partition", "85%" ); 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.displayIndex = iniState.getValueI( "window", "display_index", 0 );
windowState.position.x = iniState.getValueI( "window", "x", -1 ); windowState.position.x = iniState.getValueI( "window", "x", -1 );
windowState.position.y = iniState.getValueI( "window", "y", -1 ); windowState.position.y = iniState.getValueI( "window", "y", -1 );
@@ -331,6 +332,7 @@ void AppConfig::save( const std::vector<std::string>& recentFiles,
iniState.setValueF( "window", "pixeldensity", windowState.pixelDensity ); iniState.setValueF( "window", "pixeldensity", windowState.pixelDensity );
iniState.setValue( "window", "panel_partition", panelPartition ); iniState.setValue( "window", "panel_partition", panelPartition );
iniState.setValue( "window", "status_bar_partition", statusBarPartition ); 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", "display_index", windowState.displayIndex );
iniState.setValueI( "window", "x", windowState.position.x ); iniState.setValueI( "window", "x", windowState.position.x );
iniState.setValueI( "window", "y", windowState.position.y ); iniState.setValueI( "window", "y", windowState.position.y );
+1
View File
@@ -90,6 +90,7 @@ struct WindowStateConfig {
bool maximized{ false }; bool maximized{ false };
std::string panelPartition; std::string panelPartition;
std::string statusBarPartition; std::string statusBarPartition;
std::string rightPanelPartition{ "75%" };
int displayIndex{ 0 }; int displayIndex{ 0 };
Vector2i position{ -1, -1 }; Vector2i position{ -1, -1 };
Uint32 lastRunVersion{ 0 }; Uint32 lastRunVersion{ 0 };
+3
View File
@@ -615,6 +615,7 @@ TabWidget::container > ImageViewer > TextView {
</RelativeLayout> </RelativeLayout>
<Tab id="treeview_tab" text='@string("project", "Project")' owns="project_view_cont" text-as-fallback="true" icon="icon(folder-open, 12dp)" /> <Tab id="treeview_tab" text='@string("project", "Project")' owns="project_view_cont" text-as-fallback="true" icon="icon(folder-open, 12dp)" />
</TabWidget> </TabWidget>
<Splitter id="right_panel_splitter" splitter-partition="100%" splitter-always-show="false" splitter-hide-on-edge="true">
<vbox> <vbox>
<Splitter id="main_splitter" lw="mp" lh="0" lw8="1" orientation="vertical"> <Splitter id="main_splitter" lw="mp" lh="0" lw8="1" orientation="vertical">
<RelativeLayout id="main_splitter_cont"> <RelativeLayout id="main_splitter_cont">
@@ -706,6 +707,8 @@ TabWidget::container > ImageViewer > TextView {
<Widget class="status_sep" lw="0" lw8="1" lh="1dp" /> <Widget class="status_sep" lw="0" lw8="1" lh="1dp" />
</statusbar> </statusbar>
</vbox> </vbox>
<RelativeLayout id="right_panel_container"></RelativeLayout>
</Splitter>
</Splitter> </Splitter>
<Image id="settings" lw="16dp" lh="16dp" lg="top|right" gravity="center" /> <Image id="settings" lw="16dp" lh="16dp" lg="top|right" gravity="center" />
<TextView id="menu_hint" class="app_hint" lw="wc" lh="wc" lg="bottom|right" visible="false" <TextView id="menu_hint" class="app_hint" lw="wc" lh="wc" lg="bottom|right" visible="false"
+21
View File
@@ -6,10 +6,12 @@
#include "fontpickercontroller.hpp" #include "fontpickercontroller.hpp"
#include "keybindingshelper.hpp" #include "keybindingshelper.hpp"
#include "pathhelper.hpp" #include "pathhelper.hpp"
#include "plugins/debugger/statusdebuggercontroller.hpp"
#include "settingsactions.hpp" #include "settingsactions.hpp"
#include "settingsmenu.hpp" #include "settingsmenu.hpp"
#include "uibuildsettings.hpp" #include "uibuildsettings.hpp"
#include "uidownloadwindow.hpp" #include "uidownloadwindow.hpp"
#include "uirightpanel.hpp"
#include "uitreeviewfs.hpp" #include "uitreeviewfs.hpp"
#include "uiwelcomescreen.hpp" #include "uiwelcomescreen.hpp"
#include "version.hpp" #include "version.hpp"
@@ -860,6 +862,8 @@ bool App::loadConfig( const LogLevel& logLevel, const Sizeu& displaySize, bool s
void App::saveConfig() { void App::saveConfig() {
if ( !mCurrentProject.empty() ) if ( !mCurrentProject.empty() )
saveSidePanelTabsOrder(); saveSidePanelTabsOrder();
if ( mRightPanel )
mRightPanel->saveState();
mConfig.save( mConfig.save(
mRecentFiles, mRecentFolders, mRecentFiles, mRecentFolders,
@@ -3810,6 +3814,17 @@ UISplitter* App::getMainSplitter() const {
return mMainSplitter; 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<StatusDebuggerController*>( element.get() );
}
StatusTerminalController* App::getStatusTerminalController() const { StatusTerminalController* App::getStatusTerminalController() const {
return mStatusTerminalController.get(); return mStatusTerminalController.get();
} }
@@ -4979,6 +4994,8 @@ void App::init( InitParameters& params ) {
mSplitter->setOpenDocumentsInMainSplit( mConfig.editor.openDocumentsInMainSplit ); mSplitter->setOpenDocumentsInMainSplit( mConfig.editor.openDocumentsInMainSplit );
mSplitter->setRestoreEditorSelectionOnFocus( mConfig.editor.restoreEditorSelectionOnFocus ); mSplitter->setRestoreEditorSelectionOnFocus( mConfig.editor.restoreEditorSelectionOnFocus );
mSplitter->setOnTabWidgetCreateCb( [this]( UITabWidget* tabWidget ) { mSplitter->setOnTabWidgetCreateCb( [this]( UITabWidget* tabWidget ) {
tabWidget->setAcceptsDropOfWidgetFn(
[]( const UIWidget* widget ) { return !widget->hasClass( "debugger-tab" ); } );
tabWidget->getTabBar()->onDoubleClick( tabWidget->getTabBar()->onDoubleClick(
[this]( const MouseEvent* ) { mSplitter->createEditorInNewTab(); } ); [this]( const MouseEvent* ) { mSplitter->createEditorInNewTab(); } );
} ); } );
@@ -5032,6 +5049,10 @@ void App::init( InitParameters& params ) {
mMainSplitter = mUISceneNode->find<UISplitter>( "main_splitter" ); mMainSplitter = mUISceneNode->find<UISplitter>( "main_splitter" );
mMainSplitter->setSplitPartition( mMainSplitter->setSplitPartition(
StyleSheetLength( mConfig.windowState.statusBarPartition ) ); StyleSheetLength( mConfig.windowState.statusBarPartition ) );
auto rightPanelSplitter = mUISceneNode->find<UISplitter>( "right_panel_splitter" );
auto rightPanelContainer = mUISceneNode->find<UILayout>( "right_panel_container" );
mRightPanel =
std::make_unique<UIRightPanel>( rightPanelSplitter, rightPanelContainer, &mConfig );
mStatusBar = mUISceneNode->find<UIStatusBar>( "status_bar" ); mStatusBar = mUISceneNode->find<UIStatusBar>( "status_bar" );
mPluginManager->setMainSplitter( mMainSplitter ); mPluginManager->setMainSplitter( mMainSplitter );
+7
View File
@@ -35,6 +35,8 @@ class DateTimeController;
class FontPickerController; class FontPickerController;
class SettingsMenu; class SettingsMenu;
class UITreeViewFS; class UITreeViewFS;
class StatusDebuggerController;
class UIRightPanel;
class App : public UICodeEditorSplitter::Client, public PluginContextProvider { class App : public UICodeEditorSplitter::Client, public PluginContextProvider {
public: public:
@@ -599,6 +601,10 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider {
UISplitter* getMainSplitter() const; UISplitter* getMainSplitter() const;
UIRightPanel* getRightPanel() const;
StatusDebuggerController* getStatusDebuggerController() const;
StatusTerminalController* getStatusTerminalController() const; StatusTerminalController* getStatusTerminalController() const;
void hideStatusTerminal(); void hideStatusTerminal();
@@ -777,6 +783,7 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider {
UIThemePtr mTheme; UIThemePtr mTheme;
UIStatusBar* mStatusBar{ nullptr }; UIStatusBar* mStatusBar{ nullptr };
UISplitter* mMainSplitter{ nullptr }; UISplitter* mMainSplitter{ nullptr };
std::unique_ptr<UIRightPanel> mRightPanel;
UIMessageBox* mCloseMsgBox{ nullptr }; UIMessageBox* mCloseMsgBox{ nullptr };
UIMenuBar* mMenuBar{ nullptr }; UIMenuBar* mMenuBar{ nullptr };
std::unique_ptr<SettingsActions> mSettingsActions; std::unique_ptr<SettingsActions> mSettingsActions;
@@ -140,8 +140,10 @@ void DebuggerClientListener::initUI() {
} }
if ( !mStackModel ) { if ( !mStackModel ) {
mStackModel = std::make_shared<StackModel>( StackTraceInfo{}, sceneNode ); mStackModel =
} std::make_shared<StackModel>( StackTraceInfo{}, sceneNode, mPlugin->mProjectPath );
} else
mStackModel->setProjectPath( mPlugin->mProjectPath );
UITableView* uiThreads = sdc->getUIThreads(); UITableView* uiThreads = sdc->getUIThreads();
uiThreads->setModel( mThreadsModel ); uiThreads->setModel( mThreadsModel );
@@ -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*/, void DebuggerPlugin::onSaveProject( const std::string& /*projectFolder*/,
const std::string& projectStatePath, const std::string& projectStatePath,
bool rewriteStateOnlyIfNeeded ) { bool rewriteStateOnlyIfNeeded ) {
@@ -579,11 +584,11 @@ void DebuggerPlugin::loadDAPConfig( const std::string& path, bool updateConfigFi
mKeyBindings["debugger-step-over"] = "f10"; mKeyBindings["debugger-step-over"] = "f10";
mKeyBindings["debugger-step-into"] = "f11"; mKeyBindings["debugger-step-into"] = "f11";
mKeyBindings["debugger-step-out"] = "shift+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"; mKeyBindings["toggle-status-app-debugger"] = "mod+6";
#else #else
mKeyBindings["toggle-status-app-debugger"] = "alt+6"; mKeyBindings["toggle-status-app-debugger"] = "alt+6";
#endif #endif
} }
if ( j.contains( "keybindings" ) ) { if ( j.contains( "keybindings" ) ) {
@@ -768,8 +773,9 @@ void DebuggerPlugin::buildSidePanelTab() {
if ( mProjectPath.empty() ) if ( mProjectPath.empty() )
return; return;
UIIcon* icon = findIcon( "debug" ); UIIcon* icon = findIcon( "debug" );
mTab = mSidePanel->add( i18n( "debugger", "Debugger" ), mTabContents, mTab =
icon ? icon->createDrawable( PixelDensity::dpToPx( 12 ) ) : nullptr ); mSidePanel->add( i18n( "debugger", "Debugger" ), mTabContents,
icon ? icon->createDrawable( PixelDensity::dpToPx( 12 ) ) : nullptr );
mTab->setId( "debugger_tab" ); mTab->setId( "debugger_tab" );
mTab->setTextAsFallback( true ); 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, FileSystem::getOSSlash() );
String::replaceAll( val, KEY_PATH_SEPARATOR_ABBR, FileSystem::getOSSlash() ); String::replaceAll( val, KEY_PATH_SEPARATOR_ABBR, FileSystem::getOSSlash() );
String::replaceAll( val, KEY_UUID, UUID().toString() ); String::replaceAll( val, KEY_UUID, UUID().toString() );
String::replaceAll( val, KEY_TIMESTAMP, String::replaceAll( val, KEY_TIMESTAMP, std::to_string( Sys::getUnixTimestamp() ) );
std::to_string( Sys::getUnixTimestamp() ) );
auto* editor = getPluginContext()->getSplitter()->getCurEditor(); auto* editor = getPluginContext()->getSplitter()->getCurEditor();
if ( getPluginContext()->getSplitter()->getCurEditor() ) { if ( getPluginContext()->getSplitter()->getCurEditor() ) {
@@ -67,6 +67,8 @@ class DebuggerPlugin : public PluginBase {
std::string getDescription() override { return Definition().description; } std::string getDescription() override { return Definition().description; }
void onSaveState( IniFile* state ) override;
void onSaveProject( const std::string& projectFolder, const std::string& projectStatePath, void onSaveProject( const std::string& projectFolder, const std::string& projectStatePath,
bool rewriteStateOnlyIfNeeded ) override; bool rewriteStateOnlyIfNeeded ) override;
@@ -1,10 +1,15 @@
#include "stackmodel.hpp" #include "stackmodel.hpp"
#include <eepp/system/filesystem.hpp>
#include <eepp/ui/uiscenenode.hpp> #include <eepp/ui/uiscenenode.hpp>
namespace ecode { namespace ecode {
StackModel::StackModel( StackTraceInfo&& stack, UISceneNode* sceneNode ) : StackModel::StackModel( StackTraceInfo&& stack, UISceneNode* sceneNode, std::string projectPath ) :
mStack( std::move( stack ) ), mSceneNode( sceneNode ) {} mStack( std::move( stack ) ),
mProjectPath( std::move( projectPath ) ),
mSceneNode( sceneNode ) {
FileSystem::dirAddSlashAtEnd( mProjectPath );
}
size_t StackModel::rowCount( const ModelIndex& ) const { size_t StackModel::rowCount( const ModelIndex& ) const {
Lock l( mResourceLock ); Lock l( mResourceLock );
@@ -45,7 +50,8 @@ Variant StackModel::data( const ModelIndex& modelIndex, ModelRole role ) const {
: Variant(); : Variant();
case Columns::SourcePath: case Columns::SourcePath:
return mStack.stackFrames[modelIndex.row()].source return mStack.stackFrames[modelIndex.row()].source
? Variant( mStack.stackFrames[modelIndex.row()].source->path ) ? Variant( displaySourcePath(
mStack.stackFrames[modelIndex.row()].source->path ) )
: Variant(); : Variant();
case Columns::Line: case Columns::Line:
return Variant( String::toString( mStack.stackFrames[modelIndex.row()].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 } // namespace ecode
@@ -19,7 +19,7 @@ class StackModel : public Model {
public: public:
enum Columns { ID, Name, SourceName, SourcePath, Line, Column }; 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; virtual size_t rowCount( const ModelIndex& ) const;
@@ -37,10 +37,15 @@ class StackModel : public Model {
void setCurrentScopeId( int scope ); void setCurrentScopeId( int scope );
void setProjectPath( const std::string& projectPath );
protected: protected:
StackTraceInfo mStack; StackTraceInfo mStack;
std::string mProjectPath;
UISceneNode* mSceneNode{ nullptr }; UISceneNode* mSceneNode{ nullptr };
int mCurrentScopeId{ 0 }; int mCurrentScopeId{ 0 };
const char* displaySourcePath( const std::string& sourcePath ) const;
}; };
} // namespace ecode } // namespace ecode
@@ -1,11 +1,45 @@
#include "statusdebuggercontroller.hpp" #include "statusdebuggercontroller.hpp"
#include "../../appconfig.hpp"
#include "../../uirightpanel.hpp"
#include "../../widgetcommandexecuter.hpp" #include "../../widgetcommandexecuter.hpp"
#include "../plugincontextprovider.hpp" #include "../plugincontextprovider.hpp"
#include "eepp/ui/uiwidgetcreator.hpp" #include "eepp/ui/uiwidgetcreator.hpp"
#include <eepp/ui/uicheckbox.hpp> #include <eepp/ui/uicheckbox.hpp>
#include <nlohmann/json.hpp>
namespace ecode { 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<std::string>& 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<std::string>() ).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 { class UIBreakpointsTableCell : public UITableCell {
public: public:
static UIBreakpointsTableCell* New( const std::string& tag, const BreakpointsModel* model, static UIBreakpointsTableCell* New( const std::string& tag, const BreakpointsModel* model,
@@ -109,6 +143,37 @@ StatusDebuggerController::StatusDebuggerController( UISplitter* mainSplitter,
PluginContextProvider* pluginContext ) : PluginContextProvider* pluginContext ) :
StatusBarElement( mainSplitter, uiSceneNode, pluginContext ) {} StatusBarElement( mainSplitter, uiSceneNode, pluginContext ) {}
StatusDebuggerController::~StatusDebuggerController() {
mEventConnections.clear();
mTabWidgetEventConnections.clear();
if ( mContainer )
mContainer->removeActionsByTag( reinterpret_cast<UintPtr>( 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() { UIWidget* StatusDebuggerController::getWidget() {
return mContainer; 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<UITab>()->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<std::string> 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<std::string> 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<UIWidget>() );
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<UintPtr>( this ) );
mContainer->runOnMainThread(
[this] {
mRightPanelDropPreview = false;
updateRightPanel();
},
Time::Zero, reinterpret_cast<UintPtr>( this ) );
}
void StatusDebuggerController::updateRightPanel() {
auto rightPanel = mContext->getRightPanel();
if ( !rightPanel || !mRightPanelContainer || !mTabWidgetSplitter )
return;
rightPanel->setPanelVisible( "debugger",
mContainer && mContainer->isVisible() &&
( mRightPanelDropPreview || rightPanelHasTabs() ) );
}
void StatusDebuggerController::createContainer() { void StatusDebuggerController::createContainer() {
if ( mContainer ) if ( mContainer )
return; return;
@@ -206,21 +472,16 @@ void StatusDebuggerController::createContainer() {
} }
</style> </style>
<hboxce id="app_debugger" lw="mp" lh="mp" visible="false"> <hboxce id="app_debugger" lw="mp" lh="mp" visible="false">
<TabWidget id="app_debugger_tab_widget" lw="0" lw8="1" lh="mp"> <vbox id="app_debugger_tabs" lw="0" lw8="1" lh="mp">
<Splitter id="debugger_threads_and_stack" layout_width="mp" lh="mp" splitter-partition="15%"> <Splitter id="debugger_threads_and_stack" layout_width="mp" lh="mp" splitter-partition="15%">
<TableView id="debugger_threads" layout_width="mp" layout_height="mp" /> <TableView id="debugger_threads" layout_width="mp" layout_height="mp" column-width-mode="percentage" column-width-mode-menu="true" />
<TableView id="debugger_stack" layout_width="mp" layout_height="mp" /> <TableView id="debugger_stack" layout_width="mp" layout_height="mp" column-width-mode="percentage" column-width-mode-menu="true" />
</Splitter> </Splitter>
<TreeView id="debugger_variables" layout_width="mp" layout_height="mp" /> <TreeView id="debugger_variables" layout_width="mp" layout_height="mp" column-width-mode="percentage" column-width-mode-menu="true" />
<TreeView id="debugger_expressions" layout_width="mp" layout_height="mp" /> <TreeView id="debugger_expressions" layout_width="mp" layout_height="mp" column-width-mode="percentage" column-width-mode-menu="true" />
<BreakpointsTableView id="debugger_breakpoints" layout_width="mp" layout_height="mp" /> <BreakpointsTableView id="debugger_breakpoints" layout_width="mp" layout_height="mp" column-width-mode="percentage" column-width-mode-menu="true" />
<CodeEditor id="debugger_console" layout_width="mp" layout_height="mp" /> <CodeEditor id="debugger_console" layout_width="mp" layout_height="mp" />
<Tab id="debugger_tab_threads_and_stack" text="@string(threads_and_stack, Threads & Stack)" owns="debugger_threads_and_stack" /> </vbox>
<Tab id="debugger_tab_variables" text="@string(variables, Variables)" owns="debugger_variables" />
<Tab id="debugger_tab_expressions" text="@string(expressions, Expressions)" owns="debugger_expressions" />
<Tab id="debugger_tab_breakpoints" text="@string(breakpoints, Breakpoints)" owns="debugger_breakpoints" />
<Tab id="debugger_tab_console" text="@string(console_output, Console Output)" owns="debugger_console" />
</TabWidget>
<vbox id="app_debugger_buttons" class="vertical_bar" lw="16dp" lh="mp"> <vbox id="app_debugger_buttons" class="vertical_bar" lw="16dp" lh="mp">
<PushButton class="expand_status_bar_panel" lw="mp" tooltip="@string(expand_panel, Expand Panel)" /> <PushButton class="expand_status_bar_panel" lw="mp" tooltip="@string(expand_panel, Expand Panel)" />
<PushButton id="app_debugger_start" class="debugger_start" lw="mp" icon="icon(debug-start, 12dp)" tooltip="@string(start, Start)" /> <PushButton id="app_debugger_start" class="debugger_start" lw="mp" icon="icon(debug-start, 12dp)" tooltip="@string(start, Start)" />
@@ -248,7 +509,7 @@ void StatusDebuggerController::createContainer() {
mContext->getStatusBar()->registerStatusBarPanel( mContainer, mContainer ); 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_and_stack", mUIThreadsSplitter );
mContainer->bind( "debugger_threads", mUIThreads ); mContainer->bind( "debugger_threads", mUIThreads );
mContainer->bind( "debugger_stack", mUIStack ); mContainer->bind( "debugger_stack", mUIStack );
@@ -256,6 +517,8 @@ void StatusDebuggerController::createContainer() {
mContainer->bind( "debugger_variables", mUIVariables ); mContainer->bind( "debugger_variables", mUIVariables );
mContainer->bind( "debugger_expressions", mUIExpressions ); mContainer->bind( "debugger_expressions", mUIExpressions );
mContainer->bind( "debugger_console", mUIConsole ); mContainer->bind( "debugger_console", mUIConsole );
mEventConnections += mContainer->connect( Event::OnVisibleChange,
[this]( const Event* ) { updateRightPanel(); } );
mContainer->bind( "app_debugger_start", mUIButStart ); mContainer->bind( "app_debugger_start", mUIButStart );
mContainer->bind( "app_debugger_stop", mUIButStop ); mContainer->bind( "app_debugger_stop", mUIButStop );
mContainer->bind( "app_debugger_pause", mUIButPause ); 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_into", mUIButStepInto );
mContainer->bind( "app_debugger_step_out", mUIButStepOut ); mContainer->bind( "app_debugger_step_out", mUIButStepOut );
createTabWidgets();
mContainer->setCommand( "next-tab", [this] { mContainer->setCommand( "next-tab", [this] {
if ( mUITabWidget ) if ( mTabWidgetSplitter && mTabWidgetSplitter->getCurWidget() ) {
mUITabWidget->focusNextTab(); auto tabWidget =
mTabWidgetSplitter->tabWidgetFromWidget( mTabWidgetSplitter->getCurWidget() );
if ( tabWidget )
tabWidget->focusNextTab();
}
} ); } );
mContainer->setCommand( "previous-tab", [this] { mContainer->setCommand( "previous-tab", [this] {
if ( mUITabWidget ) if ( mTabWidgetSplitter && mTabWidgetSplitter->getCurWidget() ) {
mUITabWidget->focusPreviousTab(); auto tabWidget =
mTabWidgetSplitter->tabWidgetFromWidget( mTabWidgetSplitter->getCurWidget() );
if ( tabWidget )
tabWidget->focusPreviousTab();
}
} ); } );
for ( int i = 1; i <= 5; i++ ) { for ( int i = 1; i <= 5; i++ ) {
mContainer->setCommand( String::format( "switch-to-tab-%d", i ), [this, i] { mContainer->setCommand( String::format( "switch-to-tab-%d", i ), [this, i] {
if ( mUITabWidget ) if ( mTabWidgetSplitter )
mUITabWidget->setTabSelected( mTabWidgetSplitter->switchToTab( i - 1 );
eeclamp<Int32>( i - 1, 0, mUITabWidget->getTabCount() - 1 ) );
} ); } );
} }
mContainer->on( Event::KeyDown, [this]( const Event* event ) { mEventConnections += mContainer->connect( Event::KeyDown, [this]( const Event* event ) {
auto ke = event->asKeyEvent(); auto ke = event->asKeyEvent();
if ( ke->getSanitizedMod() == 0 && ke->getKeyCode() == EE::Window::KEY_ESCAPE && if ( ke->getSanitizedMod() == 0 && ke->getKeyCode() == EE::Window::KEY_ESCAPE &&
mSplitter->getCurEditor() ) { mSplitter->getCurEditor() ) {
@@ -313,19 +585,12 @@ void StatusDebuggerController::createContainer() {
mUIThreads->setAutoExpandOnSingleColumn( true ); mUIThreads->setAutoExpandOnSingleColumn( true );
mUIStack->setAutoColumnsWidth( true );
mUIStack->setFitAllColumnsToWidget( true );
mUIStack->setMainColumn( 1 ); mUIStack->setMainColumn( 1 );
mUIVariables->setAutoColumnsWidth( true ); mUIVariables->setMainColumn( 1 );
mUIVariables->setFitAllColumnsToWidget( true );
mUIBreakpoints->setAutoColumnsWidth( true );
mUIBreakpoints->setFitAllColumnsToWidget( true );
mUIBreakpoints->setMainColumn( 1 ); mUIBreakpoints->setMainColumn( 1 );
mUIExpressions->setAutoColumnsWidth( true );
mUIExpressions->setFitAllColumnsToWidget( true );
mUIExpressions->setMainColumn( 1 ); mUIExpressions->setMainColumn( 1 );
mUIConsole->setLocked( true ); mUIConsole->setLocked( true );
@@ -333,7 +598,7 @@ void StatusDebuggerController::createContainer() {
mUIConsole->setShowLineNumber( false ); mUIConsole->setShowLineNumber( false );
mUIConsole->getDocument().reset(); mUIConsole->getDocument().reset();
mUIConsole->setScrollY( mUIConsole->getMaxScroll().y ); 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; mScrollLocked = mUIConsole->getMaxScroll().y == mUIConsole->getScroll().y;
} ); } );
} }
@@ -3,8 +3,11 @@
#include "../../uistatusbar.hpp" #include "../../uistatusbar.hpp"
#include "models/breakpointsmodel.hpp" #include "models/breakpointsmodel.hpp"
#include <eepp/core/containers.hpp>
#include <eepp/scene/eventconnection.hpp>
#include <eepp/system/luapattern.hpp> #include <eepp/system/luapattern.hpp>
#include <eepp/ui/tools/uicodeeditorsplitter.hpp> #include <eepp/ui/tools/uicodeeditorsplitter.hpp>
#include <eepp/ui/tools/uitabwidgetsplitter.hpp>
#include <eepp/ui/uicodeeditor.hpp> #include <eepp/ui/uicodeeditor.hpp>
#include <eepp/ui/uirelativelayout.hpp> #include <eepp/ui/uirelativelayout.hpp>
#include <eepp/ui/uiscenenode.hpp> #include <eepp/ui/uiscenenode.hpp>
@@ -34,7 +37,7 @@ class UIBreakpointsTableView : public UITableView {
UIWidget* createCell( UIWidget* rowWidget, const ModelIndex& index ); UIWidget* createCell( UIWidget* rowWidget, const ModelIndex& index );
}; };
class StatusDebuggerController : public StatusBarElement { class StatusDebuggerController : public StatusBarElement, public UITabWidgetSplitter::Client {
public: public:
enum class State { NotStarted, Running, Paused }; enum class State { NotStarted, Running, Paused };
@@ -43,7 +46,11 @@ class StatusDebuggerController : public StatusBarElement {
StatusDebuggerController( UISplitter* mainSplitter, UISceneNode* uiSceneNode, StatusDebuggerController( UISplitter* mainSplitter, UISceneNode* uiSceneNode,
PluginContextProvider* pluginContext ); PluginContextProvider* pluginContext );
virtual ~StatusDebuggerController() {}; virtual ~StatusDebuggerController();
virtual void show();
virtual void hide();
UIWidget* getWidget(); UIWidget* getWidget();
@@ -71,8 +78,14 @@ class StatusDebuggerController : public StatusBarElement {
void setDebuggingState( State state ); void setDebuggingState( State state );
const std::string& saveLayout();
std::function<void( StatusDebuggerController*, UIWidget* )> onWidgetCreated{ nullptr }; std::function<void( StatusDebuggerController*, UIWidget* )> onWidgetCreated{ nullptr };
void onTabCreated( UITab* tab, UIWidget* widget );
void onWidgetFocusChange( UIWidget* widget );
protected: protected:
UIHLinearLayoutCommandExecuter* mContainer{ nullptr }; UIHLinearLayoutCommandExecuter* mContainer{ nullptr };
UITableView* mUIThreads{ nullptr }; UITableView* mUIThreads{ nullptr };
@@ -90,9 +103,32 @@ class StatusDebuggerController : public StatusBarElement {
UIPushButton* mUIButStepOver{ nullptr }; UIPushButton* mUIButStepOver{ nullptr };
UIPushButton* mUIButStepOut{ nullptr }; UIPushButton* mUIButStepOut{ nullptr };
UITabWidget* mUITabWidget{ nullptr }; UITabWidget* mUITabWidget{ nullptr };
UITabWidget* mUIRightTabWidget{ nullptr };
UITabWidgetSplitter* mTabWidgetSplitter{ nullptr };
UILayout* mDebuggerTabsContainer{ nullptr };
UILayout* mRightPanelContainer{ nullptr };
Scene::EventConnectionList mEventConnections;
UnorderedMap<UITabWidget*, Scene::EventConnectionList> mTabWidgetEventConnections;
bool mScrollLocked{ true }; bool mScrollLocked{ true };
bool mRestoringLayout{ false };
bool mRightPanelDropPreview{ false };
std::string mSerializedLayout;
void createContainer(); void createContainer();
void createTabWidgets();
void restoreTabLayout();
void saveTabLayout();
bool rightPanelHasTabs() const;
void beginRightPanelDropPreview();
void endRightPanelDropPreview();
void updateRightPanel();
}; };
} // namespace ecode } // namespace ecode
@@ -19,6 +19,7 @@ class Font;
namespace UI { namespace UI {
class UISplitter; class UISplitter;
class UILayout;
class UITabWidget; class UITabWidget;
class UISceneNode; class UISceneNode;
class UICodeEditor; class UICodeEditor;
@@ -58,6 +59,8 @@ class ProjectDirectoryTree;
struct TerminalConfig; struct TerminalConfig;
class UIMainLayout; class UIMainLayout;
class UITreeViewFS; class UITreeViewFS;
class StatusDebuggerController;
class UIRightPanel;
class PluginContextProvider { class PluginContextProvider {
public: public:
@@ -65,6 +68,10 @@ class PluginContextProvider {
virtual UISplitter* getMainSplitter() const = 0; virtual UISplitter* getMainSplitter() const = 0;
virtual UIRightPanel* getRightPanel() const = 0;
virtual StatusDebuggerController* getStatusDebuggerController() const = 0;
virtual UITreeViewFS* getProjectTreeView() const = 0; virtual UITreeViewFS* getProjectTreeView() const = 0;
virtual void hideGlobalSearchBar() = 0; virtual void hideGlobalSearchBar() = 0;
+101
View File
@@ -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
+50
View File
@@ -0,0 +1,50 @@
#ifndef ECODE_UIRIGHTPANEL_HPP
#define ECODE_UIRIGHTPANEL_HPP
#include <eepp/core/containers.hpp>
#include <eepp/ui/uirelativelayout.hpp>
#include <eepp/ui/uisplitter.hpp>
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<std::string, Panel> mPanels;
bool mVisible{ false };
void updateVisibility();
};
} // namespace ecode
#endif // ECODE_UIRIGHTPANEL_HPP