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:**
- Performance is the absolute key in `eepp`.
- Favor stack-allocated memory over heap allocations whenever possible.
- Prefer eepp's internal container layer when it provides the required semantics. Check
`include/eepp/core/containers.hpp`, `small_vector.hpp`, `lrucache.hpp`, and related core
containers before introducing standard-library or third-party containers directly.
- Also consider `include/eepp/core/small_function.hpp` for frequently stored callbacks with
known, bounded capture sizes. It is not a general replacement for `std::function`: use it only
when its inline-capacity, callable semantics, and object-size tradeoff fit the concrete use.
- Any heap allocation must be heavily justified.
- Exercise reason: maximize stack use for speed, but actively calculate boundaries to prevent stack-overflows.
- Review the memory layout of every new or materially changed struct and class. Order members
and select appropriately sized enum/integer storage to minimize alignment padding, and verify
meaningful changes with compiler layout data or `sizeof` instead of guessing. Do not use packed
layouts or otherwise force misaligned access, and preserve public ABI unless the change is
explicitly authorized.
- Before finalizing C++ changes, perform an explicit allocation audit:
- Review every heap allocation, string copy, container insertion, `std::function`, lambda capture, and async handoff introduced or touched by the change.
- Prefer move captures for owned temporary strings, buffers, vectors, and other heap-backed objects passed into lambdas.
+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/`.
* **General Context:** The `README.md` at the root directory contains deeper project details.
## Localization
The locale catalogs under `bin/assets/i18n/` belong to **ecode**; the other applications are not
currently localized. Whenever an ecode feature introduces or changes a user-facing i18n key, add or
update that key in every catalog in this directory. Do not rely only on the fallback string embedded
in the source. Keep all locale files structurally valid and verify that every supported catalog
contains the new or renamed key.
## C++ Virtual Method Style
Follow the convention already used by the class being edited. In particular, when a class declares
virtual methods without the `override` specifier, do not introduce `override` on new methods in that
class. Mixing the styles can enable Clang's inconsistent-missing-override warnings for the existing
declarations. A class-wide conversion is a separate change and must update all applicable methods
together.
## Namespace Style
Follow eepp's established namespace style: prefer the appropriate `using namespace EE::...`
declarations and unqualified eepp type names, such as `UISplitter`, over repeatedly spelling fully
qualified names such as `EE::UI::UISplitter`. Keep explicit qualification only where it is required
to resolve ambiguity or avoid importing an unusually broad namespace into an unsuitable scope.
+6 -4
View File
@@ -5,20 +5,22 @@ This project relies on a comprehensive suite of unit tests to prevent regression
## Running Tests
The test binary manages its own current working directory, so you can execute it from anywhere.
* **Prefer the release test binary during normal development:**
When AddressSanitizer or other debug-only diagnostics are not required, build and run `bin/unit_tests/eepp-unit_tests`. The optimized release suite is substantially faster and should be the default for iterative testing. Use `bin/unit_tests/eepp-unit_tests-debug` when investigating memory safety, assertions, or other behavior that specifically requires the debug configuration.
* **Default Execution for Agents on Linux & FreeBSD:**
Always run unit tests through the project wrapper unless the user explicitly asks for a different harness:
`projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug`
`projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests`
* **Why the wrapper is required:**
Tests open ~400 individual windows. The wrapper runs them in an isolated framebuffer, enables race-safe automatic display selection for concurrent agent test runs, sets the default screen to `1280x1024x24`, and injects `ASAN_OPTIONS=detect_leaks=0` automatically.
* **Do not skip the wrapper for filtered tests:**
A focused test still needs the same wrapper:
`projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests-debug --filter="FontRendering.*Offset*"`
`projects/scripts/xvfb-run-eepp bin/unit_tests/eepp-unit_tests --filter="FontRendering.*Offset*"`
* **Fallback only when the wrapper itself fails:**
If `projects/scripts/xvfb-run-eepp` fails before launching the test binary, report that wrapper failure and then use this fallback to keep verification moving:
`ASAN_OPTIONS=detect_leaks=0 xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests-debug`
`xvfb-run -a -s "-screen 0 1280x1024x24" bin/unit_tests/eepp-unit_tests`
Do not use plain `xvfb-run` as the first attempt for GUI/unit tests.
* **Direct Execution (Only for non-window tests or explicit user requests):**
`bin/unit_tests/eepp-unit_tests-debug`
`bin/unit_tests/eepp-unit_tests`
* **Filtering Tests:**
Use the `--filter` parameter to run specific tests (supports glob patterns).
Keep the wrapper in front of the binary unless the test is known not to create windows.
+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_folder">Einen Ordner auswählen</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_open_link">Link öffnen</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_folder">Select a folder</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_open_link">Open Link</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_folder">Sélectionner un dossier</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_open_link">Ouvrir un lien</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_folder">选择文件夹</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_open_link">打开链接</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
Read [cursor](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor) documentation.
@@ -1934,8 +1958,8 @@ Sets the [skin](#skin) tint color.
### splitter-always-show
Sets if the splitter divisor is always visible. If false it will be shown only if two views are
attached to the splitter.
Sets whether the splitter divisor remains visible whenever two views are attached. This property
overrides `splitter-hide-on-edge`.
* Applicable to: EE::UI::UISplitter (Splitter)
* Data Type: [boolean](#boolean-data-type)
@@ -1943,6 +1967,17 @@ attached to the splitter.
---
### splitter-hide-on-edge
Hides the splitter divisor when its percentage partition is exactly `0%` or `100%`, allowing one
view to consume the complete splitter. This has no effect when `splitter-always-show` is `true`.
* Applicable to: EE::UI::UISplitter (Splitter)
* Data Type: [boolean](#boolean-data-type)
* Default value: `false`
---
### splitter-partition
Sets the space occupied by the first view contained by the splitter.
@@ -6,6 +6,7 @@
#include <eepp/ui/uitablecell.hpp>
#include <eepp/ui/uitableheadercolumn.hpp>
#include <eepp/ui/uitablerow.hpp>
#include <nlohmann/json_fwd.hpp>
#include <unordered_map>
using namespace EE::Math;
@@ -20,6 +21,8 @@ namespace EE { namespace UI { namespace Abstract {
class EE_API UIAbstractTableView : public UIAbstractView {
public:
enum class ColumnWidthMode : Uint8 { Pixels, Percentage };
enum TableFlags : Uint32 {
TableFlagNone = 0,
TableFlagHeaders = ( 1 << 0 ),
@@ -85,6 +88,30 @@ class EE_API UIAbstractTableView : public UIAbstractView {
const Float& getColumnWidth( const size_t& colIndex ) const;
Float getColumnWidthPercentage( const size_t& colIndex ) const;
std::vector<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 );
bool getAutoExpandOnSingleColumn() const;
@@ -180,6 +207,7 @@ class EE_API UIAbstractTableView : public UIAbstractView {
Float minHeight{ 0 };
Float maxWidth{ 0 };
Float width{ 0 };
Float percentage{ 0 };
bool visible{ true };
bool manuallySet{ false };
UIPushButton* widget{ nullptr };
@@ -210,6 +238,10 @@ class EE_API UIAbstractTableView : public UIAbstractView {
std::function<void( UITableCell* )> mSetupCellCb;
Float mRowHeaderWidth{ 0 };
Uint32 mTableFlags{ UITABLE_DEFAULT_FLAGS };
ColumnWidthMode mColumnWidthMode{ ColumnWidthMode::Pixels };
bool mColumnWidthModeMenuEnabled{ false };
bool mUpdatingColumnsForScrollbars{ false };
std::string mPendingSerializedColumnWidths;
virtual ~UIAbstractTableView();
@@ -231,6 +263,8 @@ class EE_API UIAbstractTableView : public UIAbstractView {
virtual void updateColumnsWidth();
void restorePendingColumnWidths();
virtual Uint32 onFocus( NodeFocusReason reason );
virtual Uint32 onFocusLoss();
@@ -252,6 +286,8 @@ class EE_API UIAbstractTableView : public UIAbstractView {
virtual void onScrollChange();
virtual void onContentSizeChange();
virtual void onRowCreated( UITableRow* row );
virtual void onSortColumn( const size_t& colIndex );
@@ -270,6 +306,10 @@ class EE_API UIAbstractTableView : public UIAbstractView {
void resetColumnData();
void updatePercentageColumnWidths();
int adjacentVisibleColumn( size_t column ) const;
void buildRowHeader();
void updateRowHeader( int realRowIndex, const ModelIndex& index, Float yOffset );
+3
View File
@@ -232,6 +232,7 @@ enum class PropertyId : Uint16 {
TabAllowSwitchTabsInEmptySpaces,
SplitterPartition,
SplitterAlwaysShow,
SplitterHideOnEdge,
DroppableHoveringColor,
TextAsFallback,
SelectOnClick,
@@ -304,6 +305,8 @@ enum class PropertyId : Uint16 {
IconSize,
SortIconSize,
MainColumn,
ColumnWidthMode,
ColumnWidthModeMenu,
RowHeaderWidth,
TableFlags,
IndentWidth,
@@ -1,6 +1,8 @@
#ifndef 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/ui/splitdirection.hpp>
#include <eepp/ui/uiscenenode.hpp>
@@ -130,6 +132,8 @@ class EE_API UITabWidgetSplitter {
void setOnTabWidgetCreateCb( std::function<void( UITabWidget* )> cb );
void setOnTabWidgetCloseCb( std::function<void( UITabWidget* )> cb );
void closeSplitter( UISplitter* splitter );
void addRemainingTabWidgets( Node* widget );
@@ -254,10 +258,13 @@ class EE_API UITabWidgetSplitter {
Float mVisualSplitEdgePercent{ 0.1 };
Mutex mTabWidgetMutex;
std::function<void( UITabWidget* )> mOnTabWidgetCreateCb;
std::function<void( UITabWidget* )> mOnTabWidgetCloseCb;
std::function<bool( SplitDirection direction, UIWidget* widget )> mCanCreateSplitFn;
TabTryCloseCallback mTabTryCloseCb;
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 );
+9
View File
@@ -1,6 +1,7 @@
#ifndef EE_UI_UISPLITTER_HPP
#define EE_UI_UISPLITTER_HPP
#include <eepp/scene/eventconnection.hpp>
#include <eepp/ui/uilayout.hpp>
namespace EE { namespace UI {
@@ -23,6 +24,10 @@ class EE_API UISplitter : public UILayout {
void setAlwaysShowSplitter( bool alwaysShowSplitter );
const bool& hideSplitterOnEdge() const;
void setHideSplitterOnEdge( bool hideSplitterOnEdge );
const StyleSheetLength& getSplitPartition() const;
void setSplitPartition( const StyleSheetLength& divisionSplit );
@@ -49,10 +54,12 @@ class EE_API UISplitter : public UILayout {
protected:
UIOrientation mOrientation;
bool mAlwaysShowSplitter;
bool mHideSplitterOnEdge;
StyleSheetLength mSplitPartition;
UIWidget* mSplitter;
UIWidget* mFirstWidget;
UIWidget* mLastWidget;
Scene::EventConnectionList mEventConnections;
UISplitter();
@@ -63,6 +70,8 @@ class EE_API UISplitter : public UILayout {
void updateFromDrag();
void updateSplitterDragFlags();
bool shouldShowSplitter() const;
};
}} // namespace EE::UI
+3 -1
View File
@@ -20,7 +20,7 @@ class EE_API UITableHeaderColumn : public UIPushButton {
protected:
UIAbstractTableView* mView;
size_t mColIndex;
mutable UIImage* mImage{nullptr};
mutable UIImage* mImage{ nullptr };
Uint32 onCalculateDrag( const Vector2f& position, const Uint32& flags );
@@ -34,6 +34,8 @@ class EE_API UITableHeaderColumn : public UIPushButton {
Uint32 onMouseClick( const Vector2i& position, const Uint32& flags );
Uint32 onMouseUp( const Vector2i& position, const Uint32& flags );
Uint32 onMouseDoubleClick( const Vector2i& position, const Uint32& flags );
Uint32 onDragStop( const Vector2i& pos, const Uint32& flags );
+2
View File
@@ -2,6 +2,7 @@
#define EE_UI_UITABWIDGET_HPP
#include <deque>
#include <eepp/scene/eventconnection.hpp>
#include <eepp/ui/splitdirection.hpp>
#include <eepp/ui/uitab.hpp>
#include <eepp/ui/uiwidget.hpp>
@@ -260,6 +261,7 @@ class EE_API UITabWidget : public UIWidget {
UIListView* mTabSwitcher{ nullptr };
TabJumpMode mTabJumpMode{ TabJumpMode::Linear };
std::function<bool( const UIWidget* widget )> mAcceptsDropOfWidgetFn;
Scene::EventConnectionList mEventConnections;
UITabWidget();
+295 -4
View File
@@ -9,6 +9,7 @@
#include <eepp/ui/uiscrollbar.hpp>
#include <eepp/window/engine.hpp>
#include <eepp/window/input.hpp>
#include <nlohmann/json.hpp>
namespace EE { namespace UI { namespace Abstract {
@@ -104,6 +105,166 @@ const Float& UIAbstractTableView::getColumnWidth( const size_t& colIndex ) const
return columnData( colIndex ).width;
}
Float UIAbstractTableView::getColumnWidthPercentage( const size_t& colIndex ) const {
return columnData( colIndex ).percentage;
}
std::vector<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() {
getSelection().clear();
for ( size_t itemIndex = 0; itemIndex < getItemCount(); ++itemIndex ) {
@@ -139,11 +300,13 @@ void UIAbstractTableView::onModelUpdate( unsigned flags ) {
[this] {
modelUpdate( mPendingUpdateFlags.exchange( 0 ) );
createOrUpdateColumns( true );
restorePendingColumnWidths();
},
Time::Zero, onModelUpdateTag );
} else {
UIAbstractView::onModelUpdate( flags );
createOrUpdateColumns( true );
restorePendingColumnWidths();
}
}
@@ -196,6 +359,9 @@ void UIAbstractTableView::createOrUpdateColumns( bool resetColumnData ) {
col.widget->setPixelsSize( col.width, getHeaderHeight() );
}
if ( mColumnWidthMode == ColumnWidthMode::Percentage )
updatePercentageColumnWidths();
if ( mAutoColumnsWidth && visibleColCount > 1 ) {
Float contentWidth = getContentSpaceWidth();
bool shouldVScrollBeVisible = shouldVerticalScrollBeVisible();
@@ -311,9 +477,92 @@ void UIAbstractTableView::onSizeChange() {
createOrUpdateColumns( false );
}
void UIAbstractTableView::onColumnSizeChange( const size_t&, bool fromUserInteraction ) {
void UIAbstractTableView::onColumnSizeChange( const size_t& colIndex, bool fromUserInteraction ) {
if ( fromUserInteraction && mAutoColumnsWidth )
mAutoColumnsWidth = false;
if ( fromUserInteraction && mColumnWidthMode == ColumnWidthMode::Percentage && getModel() ) {
int adjacent = adjacentVisibleColumn( colIndex );
Float contentWidth = getContentSpaceWidth();
if ( adjacent >= 0 && contentWidth > 0 ) {
auto& column = columnData( colIndex );
auto& sibling = columnData( adjacent );
Float combined = column.percentage + sibling.percentage;
Float minPercentage = column.minWidth / contentWidth * 100.f;
Float siblingMinPercentage = sibling.minWidth / contentWidth * 100.f;
column.percentage = eeclamp( column.width / contentWidth * 100.f, minPercentage,
combined - siblingMinPercentage );
sibling.percentage = combined - column.percentage;
updatePercentageColumnWidths();
updateHeaderSize();
}
}
}
void UIAbstractTableView::updatePercentageColumnWidths() {
if ( !getModel() )
return;
Float contentWidth = getContentSpaceWidth();
Float totalPercentage = 0;
int visibleColumns = 0;
for ( size_t i = 0; i < getModel()->columnCount(); ++i ) {
if ( !isColumnHidden( i ) ) {
totalPercentage += columnData( i ).percentage;
visibleColumns++;
}
}
if ( totalPercentage <= 0 && visibleColumns > 0 ) {
for ( size_t i = 0; i < getModel()->columnCount(); ++i )
if ( !isColumnHidden( i ) )
columnData( i ).percentage = 100.f / visibleColumns;
totalPercentage = 100.f;
}
contentWidth = eefloor( contentWidth );
Float cumulativePercentage = 0;
Float previousBoundary = 0;
Float assignedWidth = 0;
for ( size_t i = 0; i < getModel()->columnCount(); ++i ) {
if ( isColumnHidden( i ) )
continue;
auto& column = columnData( i );
cumulativePercentage += column.percentage;
Float boundary = eefloor( contentWidth * cumulativePercentage / totalPercentage + 0.5f );
Float width = boundary - previousBoundary;
Float minWidth = eeceil( column.minWidth );
width = column.maxWidth != 0
? eeclamp( width, minWidth, eemax( minWidth, eefloor( column.maxWidth ) ) )
: eemax( width, minWidth );
column.setWidth( width, true );
assignedWidth += width;
previousBoundary = boundary;
}
Float overflow = assignedWidth - contentWidth;
for ( size_t i = getModel()->columnCount(); overflow > 0 && i > 0; --i ) {
if ( isColumnHidden( i - 1 ) )
continue;
auto& column = columnData( i - 1 );
Float shrink = eemin( overflow, column.width - eeceil( column.minWidth ) );
if ( shrink > 0 ) {
column.setWidth( column.width - shrink, true );
overflow -= shrink;
}
}
for ( size_t i = 0; i < getModel()->columnCount(); ++i ) {
auto& column = columnData( i );
if ( column.widget && !isColumnHidden( i ) )
column.widget->setPixelsSize( column.width, getHeaderHeight() );
}
}
int UIAbstractTableView::adjacentVisibleColumn( size_t column ) const {
if ( !getModel() )
return -1;
for ( size_t i = column + 1; i < getModel()->columnCount(); ++i )
if ( !isColumnHidden( i ) )
return i;
for ( size_t i = column; i > 0; --i )
if ( !isColumnHidden( i - 1 ) )
return i - 1;
return -1;
}
Float UIAbstractTableView::getMaxColumnContentWidth( const size_t&, bool ) {
@@ -322,6 +571,10 @@ Float UIAbstractTableView::getMaxColumnContentWidth( const size_t&, bool ) {
void UIAbstractTableView::onColumnResizeToContent( const size_t& colIndex ) {
columnData( colIndex ).setWidth( getMaxColumnContentWidth( colIndex, true ) );
if ( mColumnWidthMode == ColumnWidthMode::Percentage ) {
onColumnSizeChange( colIndex, true );
return;
}
createOrUpdateColumns( false );
}
@@ -582,6 +835,31 @@ void UIAbstractTableView::onScrollChange() {
mHeader->setPixelsPosition( mRowHeaderWidth + -mScrollOffset.x, 0 );
}
void UIAbstractTableView::onContentSizeChange() {
if ( mUpdatingColumnsForScrollbars ) {
UIScrollableWidget::onContentSizeChange();
return;
}
bool verticalScrollWasVisible = mVScroll->isVisible();
UIScrollableWidget::onContentSizeChange();
const bool columnsDependOnContentWidth =
mColumnWidthMode == ColumnWidthMode::Percentage || mAutoColumnsWidth ||
( mAutoExpandOnSingleColumn && visibleColumnCount() == 1 );
if ( !columnsDependOnContentWidth || verticalScrollWasVisible == mVScroll->isVisible() )
return;
mUpdatingColumnsForScrollbars = true;
for ( int iteration = 0; iteration < 2; ++iteration ) {
bool visibilityUsedForColumns = mVScroll->isVisible();
createOrUpdateColumns( false );
UIScrollableWidget::onContentSizeChange();
if ( visibilityUsedForColumns == mVScroll->isVisible() )
break;
}
mUpdatingColumnsForScrollbars = false;
}
void UIAbstractTableView::bindNavigationClick( UIWidget* widget ) {
mWidgetsClickCbId[widget].push_back(
widget->on( Event::MouseDoubleClick, [this]( const Event* event ) {
@@ -1012,6 +1290,14 @@ bool UIAbstractTableView::applyProperty( const StyleSheetProperty& attribute ) {
case PropertyId::MainColumn:
setMainColumn( attribute.asInt() );
break;
case PropertyId::ColumnWidthMode:
setColumnWidthMode( String::iequals( attribute.getValue(), "percentage" )
? ColumnWidthMode::Percentage
: ColumnWidthMode::Pixels );
break;
case PropertyId::ColumnWidthModeMenu:
setColumnWidthModeMenuEnabled( attribute.asBool() );
break;
case PropertyId::RowHeaderWidth:
setRowHeaderWidth(
lengthFromValue( attribute.getValue(), PropertyRelativeTarget::None ) );
@@ -1063,6 +1349,10 @@ std::string UIAbstractTableView::getPropertyString( const PropertyDefinition* pr
return String::fromFloat( (Float)getSortIconSize(), "px" );
case PropertyId::MainColumn:
return String::toString( (Int64)getMainColumn() );
case PropertyId::ColumnWidthMode:
return getColumnWidthMode() == ColumnWidthMode::Percentage ? "percentage" : "pixels";
case PropertyId::ColumnWidthModeMenu:
return isColumnWidthModeMenuEnabled() ? "true" : "false";
case PropertyId::RowHeaderWidth:
return String::fromFloat( getRowHeaderWidth(), "px" );
case PropertyId::TableFlags: {
@@ -1099,9 +1389,10 @@ std::string UIAbstractTableView::getPropertyString( const PropertyDefinition* pr
std::vector<PropertyId> UIAbstractTableView::getPropertiesImplemented() const {
auto props = UIAbstractView::getPropertiesImplemented();
props.insert( props.end(),
{ PropertyId::RowHeight, PropertyId::IconSize, PropertyId::SortIconSize,
PropertyId::MainColumn, PropertyId::RowHeaderWidth, PropertyId::TableFlags } );
props.insert( props.end(), { PropertyId::RowHeight, PropertyId::IconSize,
PropertyId::SortIconSize, PropertyId::MainColumn,
PropertyId::ColumnWidthMode, PropertyId::ColumnWidthModeMenu,
PropertyId::RowHeaderWidth, PropertyId::TableFlags } );
return props;
}
@@ -336,6 +336,10 @@ void StyleSheetSpecification::registerDefaultProperties() {
.setType( PropertyType::NumberLength );
registerProperty( PropertyId::MainColumn, "main-column", "0" )
.setType( PropertyType::NumberInt );
registerProperty( PropertyId::ColumnWidthMode, "column-width-mode", "pixels" )
.setType( PropertyType::String );
registerProperty( PropertyId::ColumnWidthModeMenu, "column-width-mode-menu", "false" )
.setType( PropertyType::Bool );
registerProperty( PropertyId::RowHeaderWidth, "row-header-width", "" )
.setType( PropertyType::NumberLength );
registerProperty( PropertyId::TableFlags, "table-flags", "" )
@@ -590,6 +594,8 @@ void StyleSheetSpecification::registerDefaultProperties() {
.setRelativeTarget( PropertyRelativeTarget::LocalBlockWidth );
registerProperty( PropertyId::SplitterAlwaysShow, "splitter-always-show", "true" )
.setType( PropertyType::Bool );
registerProperty( PropertyId::SplitterHideOnEdge, "splitter-hide-on-edge", "false" )
.setType( PropertyType::Bool );
registerProperty( PropertyId::DroppableHoveringColor, "droppable-hovering-color", "#FFFFFF20" )
.setType( PropertyType::Color );
@@ -1926,6 +1926,8 @@ void UICodeEditorSplitter::onTabClosed( const TabEvent* tabEvent ) {
Node* remainingNode = tabWidget == splitter->getFirstWidget()
? splitter->getLastWidget()
: splitter->getFirstWidget();
// Keep the surviving subtree out of closeSplitter()'s recursive bookkeeping.
remainingNode->detach();
closeSplitter( splitter );
eeASSERT( parent->getChildCount() == 0 );
remainingNode->setParent( parent );
+27 -8
View File
@@ -17,7 +17,13 @@ UITabWidgetSplitter::UITabWidgetSplitter( UITabWidgetSplitter::Client* client,
UISceneNode* sceneNode ) :
mUISceneNode( sceneNode ), mClient( client ) {}
UITabWidgetSplitter::~UITabWidgetSplitter() {}
UITabWidgetSplitter::~UITabWidgetSplitter() {
mTabWidgetEventConnections.clear();
mWidgetEventConnections.clear();
mTabWidgets.clear();
mCurWidget = nullptr;
mClient = nullptr;
}
UITabWidget* UITabWidgetSplitter::tabWidgetFromWidget( UIWidget* widget ) const {
if ( widget )
@@ -49,15 +55,15 @@ void UITabWidgetSplitter::setCurrentWidget( UIWidget* curWidget ) {
std::pair<UITab*, UIWidget*>
UITabWidgetSplitter::createWidgetInTabWidget( UITabWidget* tabWidget, UIWidget* widget,
const std::string& tabName, bool focus ) {
eeASSERT( curWidgetExists() );
if ( nullptr == tabWidget )
return std::make_pair( (UITab*)nullptr, (UIWidget*)nullptr );
UITab* tab = tabWidget->add( tabName, widget );
widget->setData( (UintPtr)tab );
widget->on( Event::OnFocusWithin, [this]( const Event* event ) {
auto& connections = mWidgetEventConnections[widget];
connections += widget->connect( Event::OnFocusWithin, [this]( const Event* event ) {
setCurrentWidget( event->getNode()->asType<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 );
UIWidget* widget = event->getNode()->asType<UIWidget>();
UITabWidget* tabWidget = tabWidgetFromWidget( widget );
@@ -157,7 +163,8 @@ UITabWidget* UITabWidgetSplitter::createTabWidget( Node* parent ) {
},
mVisualSplitEdgePercent );
}
tabWidget->on( Event::OnTabSelected, [this]( const Event* event ) {
auto& connections = mTabWidgetEventConnections[tabWidget];
connections += tabWidget->connect( Event::OnTabSelected, [this]( const Event* event ) {
UITabWidget* tabWidget = event->getNode()->asType<UITabWidget>();
eeASSERT( nullptr != tabWidget && nullptr != tabWidget->getTabSelected() &&
nullptr != tabWidget->getTabSelected()->getOwnedWidget() );
@@ -175,7 +182,7 @@ UITabWidget* UITabWidgetSplitter::createTabWidget( Node* parent ) {
}
return false;
} );
tabWidget->on( Event::OnTabClosed, [this]( const Event* event ) {
connections += tabWidget->connect( Event::OnTabClosed, [this]( const Event* event ) {
onTabClosed( static_cast<const TabEvent*>( event ) );
} );
if ( mOnTabWidgetCreateCb )
@@ -285,6 +292,7 @@ void UITabWidgetSplitter::closeTab( UIWidget* widget,
UITabWidget::FocusTabBehavior focusTabBehavior ) {
if ( widget ) {
UITabWidget* tabWidget = tabWidgetFromWidget( widget );
mWidgetEventConnections.erase( widget );
if ( tabWidget )
tabWidget->removeTab( (UITab*)widget->getData(), true, false, focusTabBehavior );
if ( mCurWidget == widget )
@@ -497,9 +505,12 @@ void UITabWidgetSplitter::closeTabWidgets( UISplitter* splitter ) {
Node* node = splitter->getFirstChild();
while ( node ) {
if ( node->isType( UI_TYPE_TABWIDGET ) ) {
auto it =
std::find( mTabWidgets.begin(), mTabWidgets.end(), node->asType<UITabWidget>() );
auto tabWidget = node->asType<UITabWidget>();
auto it = std::find( mTabWidgets.begin(), mTabWidgets.end(), tabWidget );
if ( it != mTabWidgets.end() ) {
if ( mOnTabWidgetCloseCb )
mOnTabWidgetCloseCb( tabWidget );
mTabWidgetEventConnections.erase( tabWidget );
Lock l( mTabWidgetMutex );
mTabWidgets.erase( it );
}
@@ -622,6 +633,9 @@ void UITabWidgetSplitter::onTabClosed( const TabEvent* tabEvent ) {
if ( tabWidget->getTabCount() == 0 ) {
UISplitter* splitter = splitterFromWidget( widget );
if ( splitter && splitter->isFull() ) {
if ( mOnTabWidgetCloseCb )
mOnTabWidgetCloseCb( tabWidget );
mTabWidgetEventConnections.erase( tabWidget );
tabWidget->close();
auto itWidget = std::find( mTabWidgets.begin(), mTabWidgets.end(), tabWidget );
if ( itWidget != mTabWidgets.end() ) {
@@ -647,6 +661,7 @@ void UITabWidgetSplitter::onTabClosed( const TabEvent* tabEvent ) {
Node* remainingNode = tabWidget == splitter->getFirstWidget()
? splitter->getLastWidget()
: splitter->getFirstWidget();
remainingNode->detach();
closeSplitter( splitter );
eeASSERT( parent->getChildCount() == 0 );
remainingNode->setParent( parent );
@@ -676,6 +691,10 @@ void UITabWidgetSplitter::setOnTabWidgetCreateCb( std::function<void( UITabWidge
mOnTabWidgetCreateCb = std::move( cb );
}
void UITabWidgetSplitter::setOnTabWidgetCloseCb( std::function<void( UITabWidget* )> cb ) {
mOnTabWidgetCloseCb = std::move( cb );
}
bool UITabWidgetSplitter::getVisualSplitting() const {
return mVisualSplitting;
}
+40 -12
View File
@@ -12,29 +12,33 @@ UISplitter::UISplitter() :
UILayout( "splitter" ),
mOrientation( UIOrientation::Horizontal ),
mAlwaysShowSplitter( true ),
mHideSplitterOnEdge( false ),
mSplitPartition( StyleSheetLength( "50%" ) ),
mFirstWidget( NULL ),
mLastWidget( NULL ) {
mFlags |= UI_OWNS_CHILDREN_POSITION;
mSplitter = UIWidget::NewWithTag( "splitter::separator" );
mSplitter->setDragEnabled( true );
mSplitter->on( Event::OnDragStart,
[this]( const Event* ) { mSplitter->pushState( UIState::StateSelected ); } );
mSplitter->on( Event::OnDragStop,
[this]( const Event* ) { mSplitter->popState( UIState::StateSelected ); } );
mEventConnections += mSplitter->connect( Event::OnDragStart, [this]( const Event* ) {
mSplitter->pushState( UIState::StateSelected );
} );
mEventConnections += mSplitter->connect( Event::OnDragStop, [this]( const Event* ) {
mSplitter->popState( UIState::StateSelected );
} );
mSplitter->setParent( this );
mSplitter->setMinWidth( 4 );
mSplitter->setMinHeight( 4 );
mSplitter->on( Event::OnSizeChange, [this]( const Event* ) {
mEventConnections += mSplitter->connect( Event::OnSizeChange, [this]( const Event* ) {
setLayoutDirty( LayoutInvalidation::ContainerLayout );
} );
mSplitter->on( Event::MouseEnter, [this]( const Event* ) {
mEventConnections += mSplitter->connect( Event::MouseEnter, [this]( const Event* ) {
getUISceneNode()->setCursor( mOrientation == UIOrientation::Horizontal ? Cursor::SizeWE
: Cursor::SizeNS );
} );
mSplitter->on( Event::MouseLeave,
[this]( const Event* ) { getUISceneNode()->setCursor( Cursor::Arrow ); } );
mSplitter->on( Event::OnPositionChange, [this]( const Event* ) {
mEventConnections += mSplitter->connect( Event::MouseLeave, [this]( const Event* ) {
getUISceneNode()->setCursor( Cursor::Arrow );
} );
mEventConnections += mSplitter->connect( Event::OnPositionChange, [this]( const Event* ) {
if ( mSplitter->isDragging() && !mDirtyLayout )
updateFromDrag();
} );
@@ -77,6 +81,17 @@ void UISplitter::setAlwaysShowSplitter( bool alwaysShowSplitter ) {
}
}
const bool& UISplitter::hideSplitterOnEdge() const {
return mHideSplitterOnEdge;
}
void UISplitter::setHideSplitterOnEdge( bool hideSplitterOnEdge ) {
if ( hideSplitterOnEdge != mHideSplitterOnEdge ) {
mHideSplitterOnEdge = hideSplitterOnEdge;
setLayoutDirty( LayoutInvalidation::ContainerLayout );
}
}
const StyleSheetLength& UISplitter::getSplitPartition() const {
return mSplitPartition;
}
@@ -127,6 +142,9 @@ bool UISplitter::applyProperty( const StyleSheetProperty& attribute ) {
case PropertyId::SplitterAlwaysShow:
setAlwaysShowSplitter( attribute.asBool() );
break;
case PropertyId::SplitterHideOnEdge:
setHideSplitterOnEdge( attribute.asBool() );
break;
case PropertyId::Orientation:
setOrientation( String::iequals( attribute.getValue(), "horizontal" )
? UIOrientation::Horizontal
@@ -149,6 +167,8 @@ std::string UISplitter::getPropertyString( const PropertyDefinition* propertyDef
return getSplitPartition().toString();
case PropertyId::SplitterAlwaysShow:
return alwaysShowSplitter() ? "true" : "false";
case PropertyId::SplitterHideOnEdge:
return hideSplitterOnEdge() ? "true" : "false";
case PropertyId::Orientation:
return getOrientation() == UIOrientation::Horizontal ? "horizontal" : "vertical";
default:
@@ -159,7 +179,7 @@ std::string UISplitter::getPropertyString( const PropertyDefinition* propertyDef
std::vector<PropertyId> UISplitter::getPropertiesImplemented() const {
auto props = UIWidget::getPropertiesImplemented();
auto local = { PropertyId::SplitterPartition, PropertyId::SplitterAlwaysShow,
PropertyId::Orientation };
PropertyId::SplitterHideOnEdge, PropertyId::Orientation };
props.insert( props.end(), local.begin(), local.end() );
return props;
}
@@ -204,7 +224,7 @@ void UISplitter::onChildCountChange( Node* child, const bool& removed ) {
void UISplitter::updateFromDrag() {
mDirtyLayout = true;
mSplitter->setVisible( !mAlwaysShowSplitter && !mLastWidget ? false : true );
mSplitter->setVisible( shouldShowSplitter() );
mSplitter->setEnabled( mSplitter->isVisible() );
if ( UIOrientation::Horizontal == mOrientation ) {
@@ -351,7 +371,7 @@ void UISplitter::updateLayout() {
return;
}
mSplitter->setVisible( !mAlwaysShowSplitter && !mLastWidget ? false : true );
mSplitter->setVisible( shouldShowSplitter() );
mSplitter->setEnabled( mSplitter->isVisible() );
Float totalSpace = mOrientation == UIOrientation::Horizontal
? mSize.getWidth() - mPaddingPx.Left - mPaddingPx.Right
@@ -426,6 +446,14 @@ void UISplitter::updateSplitterDragFlags() {
: UI_DRAG_HORIZONTAL );
}
bool UISplitter::shouldShowSplitter() const {
if ( !mLastWidget )
return false;
return mAlwaysShowSplitter || !mHideSplitterOnEdge ||
mSplitPartition.getUnit() != StyleSheetLength::Percentage ||
( mSplitPartition.getValue() > 0.f && mSplitPartition.getValue() < 100.f );
}
Uint32 UISplitter::onMessage( const NodeMessage* Msg ) {
switch ( Msg->getMsg() ) {
case NodeMessage::LayoutAttributeChange: {
+27 -1
View File
@@ -1,4 +1,6 @@
#include <eepp/ui/abstract/uiabstracttableview.hpp>
#include <eepp/ui/uimenuitem.hpp>
#include <eepp/ui/uipopupmenu.hpp>
#include <eepp/ui/uiscenenode.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 ) );
if ( mDragPoint != pos && std::abs( mDragPoint.x - pos.x ) > 1.f ) {
Sizef dragDiff( ( Float )( mDragPoint.x - pos.x ), 0 );
Sizef dragDiff( (Float)( mDragPoint.x - pos.x ), 0 );
if ( onDrag( pos, flags, dragDiff ) ) {
mDragPoint = pos;
eventDispatcher->setNodeDragging( this );
@@ -77,6 +79,30 @@ Uint32 UITableHeaderColumn::onMouseClick( const Vector2i& position, const Uint32
return UIPushButton::onMouseClick( position, flags );
}
Uint32 UITableHeaderColumn::onMouseUp( const Vector2i& position, const Uint32& flags ) {
if ( ( flags & EE_BUTTON_RMASK ) && mView->isColumnWidthModeMenuEnabled() ) {
auto* menu = UIPopUpMenu::New();
menu->addRadioButton( i18n( "uitable_fit_columns_to_view", "Fit Columns to View" ),
mView->getColumnWidthMode() ==
UIAbstractTableView::ColumnWidthMode::Percentage )
->setId( "percentage" );
menu->addRadioButton( i18n( "uitable_free_column_widths", "Free Column Widths" ),
mView->getColumnWidthMode() ==
UIAbstractTableView::ColumnWidthMode::Pixels )
->setId( "pixels" );
menu->on( Event::OnItemClicked, [view = mView]( const Event* event ) {
if ( !event->getNode()->isType( UI_TYPE_MENUITEM ) )
return;
view->setColumnWidthMode( event->getNode()->getId() == "percentage"
? UIAbstractTableView::ColumnWidthMode::Percentage
: UIAbstractTableView::ColumnWidthMode::Pixels );
} );
menu->setCloseOnHide( true );
menu->showAtScreenPosition( position.asFloat() );
}
return UIPushButton::onMouseUp( position, flags );
}
Uint32 UITableHeaderColumn::onDrag( const Vector2f& position, const Uint32&,
const Sizef& dragDiff ) {
Vector2f localPos( convertToNodeSpace( position ) );
+8 -2
View File
@@ -87,8 +87,10 @@ UITabWidget::UITabWidget() :
mTabScroll = UIScrollBar::NewHorizontalWithTag( "scrollbarmini" );
mTabScroll->setParent( mTabBar );
mTabScroll->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::WrapContent );
mTabScroll->on( Event::OnSizeChange, [this]( const Event* ) { updateScrollBar(); } );
mTabScroll->on( Event::OnValueChange, [this]( const Event* ) { updateScroll(); } );
mEventConnections +=
mTabScroll->connect( Event::OnSizeChange, [this]( const Event* ) { updateScrollBar(); } );
mEventConnections +=
mTabScroll->connect( Event::OnValueChange, [this]( const Event* ) { updateScroll(); } );
onSizeChange();
@@ -560,6 +562,10 @@ UITab* UITabWidget::add( const String& text, UINode* nodeOwned, DrawablePtr icon
UITabWidget* UITabWidget::add( UITab* tab ) {
tab->setParent( mTabBar );
if ( tab->getOwnedWidget() && tab->getOwnedWidget()->isWidget() )
tab->getOwnedWidget()->asType<UIWidget>()->setLayoutSizePolicy( SizePolicy::Fixed,
SizePolicy::Fixed );
refreshOwnedWidget( tab );
mTabs.push_back( tab );
@@ -3,6 +3,8 @@
#include <eepp/system/sys.hpp>
#include <eepp/ui/tools/uitabwidgetsplitter.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uiscrollbar.hpp>
#include <eepp/ui/uitableview.hpp>
#include <eepp/ui/uiwidget.hpp>
#include <nlohmann/json.hpp>
@@ -21,6 +23,415 @@ class TestClient : public UITabWidgetSplitter::Client {
void onWidgetFocusChange( UIWidget* ) override { focusChangeCount++; }
};
class TransferTestTabWidget : public UITabWidget {
public:
static TransferTestTabWidget* New() { return eeNew( TransferTestTabWidget, () ); }
void detachTab( UITab* tab ) { removeTab( tab, false, false, false ); }
const Sizef& getContainerPixelsSize() const { return mNodeContainer->getPixelsSize(); }
};
class ThreeColumnModel : public Model {
public:
explicit ThreeColumnModel( size_t rows = 1 ) : mRows( rows ) {}
size_t rowCount( const ModelIndex& = ModelIndex() ) const override { return mRows; }
size_t columnCount( const ModelIndex& = ModelIndex() ) const override { return 3; }
std::string columnName( const size_t& column ) const override {
return String::format( "Column %zu", column );
}
ModelIndex index( int row, int column,
const ModelIndex& parent = ModelIndex() ) const override {
return row == 0 && column >= 0 && column < 3 && !parent.isValid()
? createIndex( row, column )
: ModelIndex{};
}
Variant data( const ModelIndex&, ModelRole = ModelRole::Display ) const override {
return "Value";
}
protected:
size_t mRows;
};
class PercentageTestTable : public UITableView {
public:
static PercentageTestTable* New() { return eeNew( PercentageTestTable, () ); }
void userResizeColumn( size_t column, Float width ) {
columnData( column ).setWidth( width, true );
onColumnSizeChange( column, true );
}
void setColumnMinimumWidth( size_t column, Float width ) {
columnData( column ).minWidth = width;
createOrUpdateColumns( false );
}
void resizeColumnToContent( size_t column, Float width ) {
mContentWidth = width;
onColumnResizeToContent( column );
}
Float getMaxColumnContentWidth( const size_t&, bool ) override { return mContentWidth; }
protected:
Float mContentWidth{ 0 };
};
UTEST( UISplitter, HideSplitterOnEdge ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - unit tests" ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
auto* splitter = UISplitter::New();
splitter->setParent( app.getUI() );
splitter->setPixelsSize( 800, 600 );
UIWidget::New()->setParent( splitter );
UIWidget::New()->setParent( splitter );
auto* separator = splitter->findByTag( "splitter::separator" );
ASSERT_TRUE( separator != nullptr );
splitter->setHideSplitterOnEdge( true );
splitter->setAlwaysShowSplitter( false );
splitter->setSplitPartition( StyleSheetLength( "100%" ) );
splitter->updateLayout();
EXPECT_FALSE( separator->isVisible() );
EXPECT_EQ( splitter->getFirstWidget()->getPixelsSize().getWidth(), 800.f );
EXPECT_EQ( splitter->getLastWidget()->getPixelsSize().getWidth(), 0.f );
splitter->setSplitPartition( StyleSheetLength( "75%" ) );
splitter->updateLayout();
app.getUI()->update( Milliseconds( 16 ) );
EXPECT_TRUE( separator->isVisible() );
splitter->setSplitPartition( StyleSheetLength( "0%" ) );
splitter->updateLayout();
EXPECT_FALSE( separator->isVisible() );
EXPECT_EQ( splitter->getFirstWidget()->getPixelsSize().getWidth(), 0.f );
EXPECT_EQ( splitter->getLastWidget()->getPixelsSize().getWidth(), 800.f );
splitter->setAlwaysShowSplitter( true );
splitter->updateLayout();
EXPECT_TRUE( separator->isVisible() );
}
UTEST( UISplitter, ControlsMatchParentChildWidth ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - unit tests" ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
auto* splitter = UISplitter::New();
splitter->setParent( app.getUI() );
splitter->setPixelsSize( 800, 600 );
auto* first = UIWidget::New();
first->setParent( splitter );
auto* last = UIWidget::New();
last->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent );
last->setParent( splitter );
splitter->setSplitPartition( StyleSheetLength( "75%" ) );
splitter->updateLayout();
EXPECT_TRUE( first->getPixelsSize().getWidth() > 590.f &&
first->getPixelsSize().getWidth() < 600.f );
EXPECT_TRUE( last->getPixelsSize().getWidth() > 190.f &&
last->getPixelsSize().getWidth() < 200.f );
}
UTEST( UIAbstractTableView, PercentageColumnWidthsScaleAndPreserveAdjacentTotal ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - unit tests" ),
UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) );
auto* table = PercentageTestTable::New();
table->setParent( app.getUI() );
table->setPixelsSize( 400, 200 );
table->setModel( std::make_shared<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 ) {
UIApplication app(
WindowSettings( 800, 600, "eepp - unit tests" ),
@@ -34,15 +445,14 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* tabWidget = splitter->createTabWidget( (Node*)app.getUI() );
bool saved = false;
splitter->registerWidgetType(
"testwidget",
{ [&]( UIWidget* ) {
saved = true;
return nlohmann::json{ { "custom_key", 42 } };
},
[]( const nlohmann::json& ) -> WidgetLoadResult {
return { nullptr, nullptr, "" };
} } );
splitter->registerWidgetType( "testwidget",
{ [&]( UIWidget* ) {
saved = true;
return nlohmann::json{ { "custom_key", 42 } };
},
[]( const nlohmann::json& ) -> WidgetLoadResult {
return { nullptr, nullptr, "" };
} } );
auto* w = UIWidget::New();
w->addClass( "testwidget" );
@@ -93,9 +503,8 @@ UTEST( UITabWidgetSplitter, Serialization ) {
splitter->createWidgetInTabWidget( tabWidget, ghost, "Ghost" );
splitter->registerWidgetType(
"known",
{ []( UIWidget* ) { return nlohmann::json{ { "val", 1 } }; },
[]( const nlohmann::json& ) -> WidgetLoadResult { return {}; } } );
"known", { []( UIWidget* ) { return nlohmann::json{ { "val", 1 } }; },
[]( const nlohmann::json& ) -> WidgetLoadResult { return {}; } } );
auto* reg = UIWidget::New();
reg->addClass( "known" );
@@ -117,13 +526,12 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* tabWidget = splitter->createTabWidget( (Node*)app.getUI() );
splitter->registerWidgetType(
"panewidget",
{ []( UIWidget* ) { return nlohmann::json{ { "pane", true } }; },
[]( const nlohmann::json& j ) -> WidgetLoadResult {
auto* w = UIWidget::New();
w->addClass( "panewidget" );
return { w, nullptr, j.value( "title", "" ) };
} } );
"panewidget", { []( UIWidget* ) { return nlohmann::json{ { "pane", true } }; },
[]( const nlohmann::json& j ) -> WidgetLoadResult {
auto* w = UIWidget::New();
w->addClass( "panewidget" );
return { w, nullptr, j.value( "title", "" ) };
} } );
auto* w1 = UIWidget::New();
w1->addClass( "panewidget" );
@@ -163,13 +571,12 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* tab1 = splitter1->createTabWidget( (Node*)app.getUI() );
splitter1->registerWidgetType(
"mywidget",
{ []( UIWidget* ) { return nlohmann::json{ { "data", "hello" } }; },
[]( const nlohmann::json& j ) -> WidgetLoadResult {
auto* w = UIWidget::New();
w->addClass( "mywidget" );
return { w, nullptr, j.value( "title", "" ) };
} } );
"mywidget", { []( UIWidget* ) { return nlohmann::json{ { "data", "hello" } }; },
[]( const nlohmann::json& j ) -> WidgetLoadResult {
auto* w = UIWidget::New();
w->addClass( "mywidget" );
return { w, nullptr, j.value( "title", "" ) };
} } );
auto* wa = UIWidget::New();
wa->addClass( "mywidget" );
@@ -186,14 +593,13 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* splitter2 = UITabWidgetSplitter::New( &client2, app.getUI() );
auto* tab2 = splitter2->createTabWidget( (Node*)app.getUI() );
splitter2->registerWidgetType(
"mywidget",
{ []( UIWidget* ) { return nlohmann::json::object(); },
[]( const nlohmann::json& j ) -> WidgetLoadResult {
auto* w = UIWidget::New();
w->addClass( "mywidget" );
return { w, nullptr, j.value( "title", "" ) };
} } );
splitter2->registerWidgetType( "mywidget",
{ []( UIWidget* ) { return nlohmann::json::object(); },
[]( const nlohmann::json& j ) -> WidgetLoadResult {
auto* w = UIWidget::New();
w->addClass( "mywidget" );
return { w, nullptr, j.value( "title", "" ) };
} } );
splitter2->fromJSON( saved );
@@ -256,14 +662,13 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* splitter1 = UITabWidgetSplitter::New( &client1, app.getUI() );
auto* tab1 = splitter1->createTabWidget( (Node*)app.getUI() );
splitter1->registerWidgetType(
"partwidget",
{ []( UIWidget* ) { return nlohmann::json::object(); },
[]( const nlohmann::json& j ) -> WidgetLoadResult {
auto* w = UIWidget::New();
w->addClass( "partwidget" );
return { w, nullptr, j.value( "title", "" ) };
} } );
splitter1->registerWidgetType( "partwidget",
{ []( UIWidget* ) { return nlohmann::json::object(); },
[]( const nlohmann::json& j ) -> WidgetLoadResult {
auto* w = UIWidget::New();
w->addClass( "partwidget" );
return { w, nullptr, j.value( "title", "" ) };
} } );
auto* w1 = UIWidget::New();
w1->addClass( "partwidget" );
@@ -287,14 +692,13 @@ UTEST( UITabWidgetSplitter, Serialization ) {
auto* splitter2 = UITabWidgetSplitter::New( &client2, app.getUI() );
splitter2->createTabWidget( (Node*)app.getUI() );
splitter2->registerWidgetType(
"partwidget",
{ []( UIWidget* ) { return nlohmann::json::object(); },
[]( const nlohmann::json& j ) -> WidgetLoadResult {
auto* w = UIWidget::New();
w->addClass( "partwidget" );
return { w, nullptr, j.value( "title", "" ) };
} } );
splitter2->registerWidgetType( "partwidget",
{ []( UIWidget* ) { return nlohmann::json::object(); },
[]( const nlohmann::json& j ) -> WidgetLoadResult {
auto* w = UIWidget::New();
w->addClass( "partwidget" );
return { w, nullptr, j.value( "title", "" ) };
} } );
splitter2->fromJSON( saved );
+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.panelPartition = iniState.getValue( "window", "panel_partition", "15%" );
windowState.statusBarPartition = iniState.getValue( "window", "status_bar_partition", "85%" );
windowState.rightPanelPartition = iniState.getValue( "window", "right_panel_partition", "75%" );
windowState.displayIndex = iniState.getValueI( "window", "display_index", 0 );
windowState.position.x = iniState.getValueI( "window", "x", -1 );
windowState.position.y = iniState.getValueI( "window", "y", -1 );
@@ -331,6 +332,7 @@ void AppConfig::save( const std::vector<std::string>& recentFiles,
iniState.setValueF( "window", "pixeldensity", windowState.pixelDensity );
iniState.setValue( "window", "panel_partition", panelPartition );
iniState.setValue( "window", "status_bar_partition", statusBarPartition );
iniState.setValue( "window", "right_panel_partition", windowState.rightPanelPartition );
iniState.setValueI( "window", "display_index", windowState.displayIndex );
iniState.setValueI( "window", "x", windowState.position.x );
iniState.setValueI( "window", "y", windowState.position.y );
+1
View File
@@ -90,6 +90,7 @@ struct WindowStateConfig {
bool maximized{ false };
std::string panelPartition;
std::string statusBarPartition;
std::string rightPanelPartition{ "75%" };
int displayIndex{ 0 };
Vector2i position{ -1, -1 };
Uint32 lastRunVersion{ 0 };
+3
View File
@@ -615,6 +615,7 @@ TabWidget::container > ImageViewer > TextView {
</RelativeLayout>
<Tab id="treeview_tab" text='@string("project", "Project")' owns="project_view_cont" text-as-fallback="true" icon="icon(folder-open, 12dp)" />
</TabWidget>
<Splitter id="right_panel_splitter" splitter-partition="100%" splitter-always-show="false" splitter-hide-on-edge="true">
<vbox>
<Splitter id="main_splitter" lw="mp" lh="0" lw8="1" orientation="vertical">
<RelativeLayout id="main_splitter_cont">
@@ -706,6 +707,8 @@ TabWidget::container > ImageViewer > TextView {
<Widget class="status_sep" lw="0" lw8="1" lh="1dp" />
</statusbar>
</vbox>
<RelativeLayout id="right_panel_container"></RelativeLayout>
</Splitter>
</Splitter>
<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"
+21
View File
@@ -6,10 +6,12 @@
#include "fontpickercontroller.hpp"
#include "keybindingshelper.hpp"
#include "pathhelper.hpp"
#include "plugins/debugger/statusdebuggercontroller.hpp"
#include "settingsactions.hpp"
#include "settingsmenu.hpp"
#include "uibuildsettings.hpp"
#include "uidownloadwindow.hpp"
#include "uirightpanel.hpp"
#include "uitreeviewfs.hpp"
#include "uiwelcomescreen.hpp"
#include "version.hpp"
@@ -860,6 +862,8 @@ bool App::loadConfig( const LogLevel& logLevel, const Sizeu& displaySize, bool s
void App::saveConfig() {
if ( !mCurrentProject.empty() )
saveSidePanelTabsOrder();
if ( mRightPanel )
mRightPanel->saveState();
mConfig.save(
mRecentFiles, mRecentFolders,
@@ -3810,6 +3814,17 @@ UISplitter* App::getMainSplitter() const {
return mMainSplitter;
}
UIRightPanel* App::getRightPanel() const {
return mRightPanel.get();
}
StatusDebuggerController* App::getStatusDebuggerController() const {
if ( !mStatusBar )
return nullptr;
auto element = mStatusBar->getStatusBarElement( "status_app_debugger" );
return static_cast<StatusDebuggerController*>( element.get() );
}
StatusTerminalController* App::getStatusTerminalController() const {
return mStatusTerminalController.get();
}
@@ -4979,6 +4994,8 @@ void App::init( InitParameters& params ) {
mSplitter->setOpenDocumentsInMainSplit( mConfig.editor.openDocumentsInMainSplit );
mSplitter->setRestoreEditorSelectionOnFocus( mConfig.editor.restoreEditorSelectionOnFocus );
mSplitter->setOnTabWidgetCreateCb( [this]( UITabWidget* tabWidget ) {
tabWidget->setAcceptsDropOfWidgetFn(
[]( const UIWidget* widget ) { return !widget->hasClass( "debugger-tab" ); } );
tabWidget->getTabBar()->onDoubleClick(
[this]( const MouseEvent* ) { mSplitter->createEditorInNewTab(); } );
} );
@@ -5032,6 +5049,10 @@ void App::init( InitParameters& params ) {
mMainSplitter = mUISceneNode->find<UISplitter>( "main_splitter" );
mMainSplitter->setSplitPartition(
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" );
mPluginManager->setMainSplitter( mMainSplitter );
+7
View File
@@ -35,6 +35,8 @@ class DateTimeController;
class FontPickerController;
class SettingsMenu;
class UITreeViewFS;
class StatusDebuggerController;
class UIRightPanel;
class App : public UICodeEditorSplitter::Client, public PluginContextProvider {
public:
@@ -599,6 +601,10 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider {
UISplitter* getMainSplitter() const;
UIRightPanel* getRightPanel() const;
StatusDebuggerController* getStatusDebuggerController() const;
StatusTerminalController* getStatusTerminalController() const;
void hideStatusTerminal();
@@ -777,6 +783,7 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider {
UIThemePtr mTheme;
UIStatusBar* mStatusBar{ nullptr };
UISplitter* mMainSplitter{ nullptr };
std::unique_ptr<UIRightPanel> mRightPanel;
UIMessageBox* mCloseMsgBox{ nullptr };
UIMenuBar* mMenuBar{ nullptr };
std::unique_ptr<SettingsActions> mSettingsActions;
@@ -140,8 +140,10 @@ void DebuggerClientListener::initUI() {
}
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();
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*/,
const std::string& projectStatePath,
bool rewriteStateOnlyIfNeeded ) {
@@ -579,11 +584,11 @@ void DebuggerPlugin::loadDAPConfig( const std::string& path, bool updateConfigFi
mKeyBindings["debugger-step-over"] = "f10";
mKeyBindings["debugger-step-into"] = "f11";
mKeyBindings["debugger-step-out"] = "shift+f11";
#if EE_PLATFORM == EE_PLATFORM_MACOS
#if EE_PLATFORM == EE_PLATFORM_MACOS
mKeyBindings["toggle-status-app-debugger"] = "mod+6";
#else
#else
mKeyBindings["toggle-status-app-debugger"] = "alt+6";
#endif
#endif
}
if ( j.contains( "keybindings" ) ) {
@@ -768,8 +773,9 @@ void DebuggerPlugin::buildSidePanelTab() {
if ( mProjectPath.empty() )
return;
UIIcon* icon = findIcon( "debug" );
mTab = mSidePanel->add( i18n( "debugger", "Debugger" ), mTabContents,
icon ? icon->createDrawable( PixelDensity::dpToPx( 12 ) ) : nullptr );
mTab =
mSidePanel->add( i18n( "debugger", "Debugger" ), mTabContents,
icon ? icon->createDrawable( PixelDensity::dpToPx( 12 ) ) : nullptr );
mTab->setId( "debugger_tab" );
mTab->setTextAsFallback( true );
@@ -1217,8 +1223,7 @@ bool DebuggerPlugin::replaceInVal( std::string& val,
String::replaceAll( val, KEY_PATH_SEPARATOR, FileSystem::getOSSlash() );
String::replaceAll( val, KEY_PATH_SEPARATOR_ABBR, FileSystem::getOSSlash() );
String::replaceAll( val, KEY_UUID, UUID().toString() );
String::replaceAll( val, KEY_TIMESTAMP,
std::to_string( Sys::getUnixTimestamp() ) );
String::replaceAll( val, KEY_TIMESTAMP, std::to_string( Sys::getUnixTimestamp() ) );
auto* editor = getPluginContext()->getSplitter()->getCurEditor();
if ( getPluginContext()->getSplitter()->getCurEditor() ) {
@@ -67,6 +67,8 @@ class DebuggerPlugin : public PluginBase {
std::string getDescription() override { return Definition().description; }
void onSaveState( IniFile* state ) override;
void onSaveProject( const std::string& projectFolder, const std::string& projectStatePath,
bool rewriteStateOnlyIfNeeded ) override;
@@ -1,10 +1,15 @@
#include "stackmodel.hpp"
#include <eepp/system/filesystem.hpp>
#include <eepp/ui/uiscenenode.hpp>
namespace ecode {
StackModel::StackModel( StackTraceInfo&& stack, UISceneNode* sceneNode ) :
mStack( std::move( stack ) ), mSceneNode( sceneNode ) {}
StackModel::StackModel( StackTraceInfo&& stack, UISceneNode* sceneNode, std::string projectPath ) :
mStack( std::move( stack ) ),
mProjectPath( std::move( projectPath ) ),
mSceneNode( sceneNode ) {
FileSystem::dirAddSlashAtEnd( mProjectPath );
}
size_t StackModel::rowCount( const ModelIndex& ) const {
Lock l( mResourceLock );
@@ -45,7 +50,8 @@ Variant StackModel::data( const ModelIndex& modelIndex, ModelRole role ) const {
: Variant();
case Columns::SourcePath:
return mStack.stackFrames[modelIndex.row()].source
? Variant( mStack.stackFrames[modelIndex.row()].source->path )
? Variant( displaySourcePath(
mStack.stackFrames[modelIndex.row()].source->path ) )
: Variant();
case Columns::Line:
return Variant( String::toString( mStack.stackFrames[modelIndex.row()].line ) );
@@ -90,4 +96,23 @@ void StackModel::setCurrentScopeId( int scope ) {
}
}
void StackModel::setProjectPath( const std::string& projectPath ) {
std::string normalizedProjectPath( projectPath );
FileSystem::dirAddSlashAtEnd( normalizedProjectPath );
{
Lock l( mResourceLock );
if ( mProjectPath == normalizedProjectPath )
return;
mProjectPath = std::move( normalizedProjectPath );
}
invalidate();
}
const char* StackModel::displaySourcePath( const std::string& sourcePath ) const {
if ( mProjectPath.empty() || sourcePath.size() < mProjectPath.size() ||
0 != sourcePath.compare( 0, mProjectPath.size(), mProjectPath ) )
return sourcePath.c_str();
return sourcePath.c_str() + mProjectPath.size();
}
} // namespace ecode
@@ -19,7 +19,7 @@ class StackModel : public Model {
public:
enum Columns { ID, Name, SourceName, SourcePath, Line, Column };
StackModel( StackTraceInfo&& stack, UISceneNode* sceneNode );
StackModel( StackTraceInfo&& stack, UISceneNode* sceneNode, std::string projectPath = {} );
virtual size_t rowCount( const ModelIndex& ) const;
@@ -37,10 +37,15 @@ class StackModel : public Model {
void setCurrentScopeId( int scope );
void setProjectPath( const std::string& projectPath );
protected:
StackTraceInfo mStack;
std::string mProjectPath;
UISceneNode* mSceneNode{ nullptr };
int mCurrentScopeId{ 0 };
const char* displaySourcePath( const std::string& sourcePath ) const;
};
} // namespace ecode
@@ -1,11 +1,45 @@
#include "statusdebuggercontroller.hpp"
#include "../../appconfig.hpp"
#include "../../uirightpanel.hpp"
#include "../../widgetcommandexecuter.hpp"
#include "../plugincontextprovider.hpp"
#include "eepp/ui/uiwidgetcreator.hpp"
#include <eepp/ui/uicheckbox.hpp>
#include <nlohmann/json.hpp>
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 {
public:
static UIBreakpointsTableCell* New( const std::string& tag, const BreakpointsModel* model,
@@ -109,6 +143,37 @@ StatusDebuggerController::StatusDebuggerController( UISplitter* mainSplitter,
PluginContextProvider* pluginContext ) :
StatusBarElement( mainSplitter, uiSceneNode, pluginContext ) {}
StatusDebuggerController::~StatusDebuggerController() {
mEventConnections.clear();
mTabWidgetEventConnections.clear();
if ( mContainer )
mContainer->removeActionsByTag( reinterpret_cast<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() {
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() {
if ( mContainer )
return;
@@ -206,21 +472,16 @@ void StatusDebuggerController::createContainer() {
}
</style>
<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%">
<TableView id="debugger_threads" layout_width="mp" layout_height="mp" />
<TableView id="debugger_stack" layout_width="mp" layout_height="mp" />
</Splitter>
<TreeView id="debugger_variables" layout_width="mp" layout_height="mp" />
<TreeView id="debugger_expressions" layout_width="mp" layout_height="mp" />
<BreakpointsTableView id="debugger_breakpoints" 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" column-width-mode="percentage" column-width-mode-menu="true" />
</Splitter>
<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" column-width-mode="percentage" column-width-mode-menu="true" />
<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" />
<Tab id="debugger_tab_threads_and_stack" text="@string(threads_and_stack, Threads & Stack)" owns="debugger_threads_and_stack" />
<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>
<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 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 );
mContainer->bind( "app_debugger_tab_widget", mUITabWidget );
mContainer->bind( "app_debugger_tabs", mDebuggerTabsContainer );
mContainer->bind( "debugger_threads_and_stack", mUIThreadsSplitter );
mContainer->bind( "debugger_threads", mUIThreads );
mContainer->bind( "debugger_stack", mUIStack );
@@ -256,6 +517,8 @@ void StatusDebuggerController::createContainer() {
mContainer->bind( "debugger_variables", mUIVariables );
mContainer->bind( "debugger_expressions", mUIExpressions );
mContainer->bind( "debugger_console", mUIConsole );
mEventConnections += mContainer->connect( Event::OnVisibleChange,
[this]( const Event* ) { updateRightPanel(); } );
mContainer->bind( "app_debugger_start", mUIButStart );
mContainer->bind( "app_debugger_stop", mUIButStop );
mContainer->bind( "app_debugger_pause", mUIButPause );
@@ -264,25 +527,34 @@ void StatusDebuggerController::createContainer() {
mContainer->bind( "app_debugger_step_into", mUIButStepInto );
mContainer->bind( "app_debugger_step_out", mUIButStepOut );
createTabWidgets();
mContainer->setCommand( "next-tab", [this] {
if ( mUITabWidget )
mUITabWidget->focusNextTab();
if ( mTabWidgetSplitter && mTabWidgetSplitter->getCurWidget() ) {
auto tabWidget =
mTabWidgetSplitter->tabWidgetFromWidget( mTabWidgetSplitter->getCurWidget() );
if ( tabWidget )
tabWidget->focusNextTab();
}
} );
mContainer->setCommand( "previous-tab", [this] {
if ( mUITabWidget )
mUITabWidget->focusPreviousTab();
if ( mTabWidgetSplitter && mTabWidgetSplitter->getCurWidget() ) {
auto tabWidget =
mTabWidgetSplitter->tabWidgetFromWidget( mTabWidgetSplitter->getCurWidget() );
if ( tabWidget )
tabWidget->focusPreviousTab();
}
} );
for ( int i = 1; i <= 5; i++ ) {
mContainer->setCommand( String::format( "switch-to-tab-%d", i ), [this, i] {
if ( mUITabWidget )
mUITabWidget->setTabSelected(
eeclamp<Int32>( i - 1, 0, mUITabWidget->getTabCount() - 1 ) );
if ( mTabWidgetSplitter )
mTabWidgetSplitter->switchToTab( i - 1 );
} );
}
mContainer->on( Event::KeyDown, [this]( const Event* event ) {
mEventConnections += mContainer->connect( Event::KeyDown, [this]( const Event* event ) {
auto ke = event->asKeyEvent();
if ( ke->getSanitizedMod() == 0 && ke->getKeyCode() == EE::Window::KEY_ESCAPE &&
mSplitter->getCurEditor() ) {
@@ -313,19 +585,12 @@ void StatusDebuggerController::createContainer() {
mUIThreads->setAutoExpandOnSingleColumn( true );
mUIStack->setAutoColumnsWidth( true );
mUIStack->setFitAllColumnsToWidget( true );
mUIStack->setMainColumn( 1 );
mUIVariables->setAutoColumnsWidth( true );
mUIVariables->setFitAllColumnsToWidget( true );
mUIVariables->setMainColumn( 1 );
mUIBreakpoints->setAutoColumnsWidth( true );
mUIBreakpoints->setFitAllColumnsToWidget( true );
mUIBreakpoints->setMainColumn( 1 );
mUIExpressions->setAutoColumnsWidth( true );
mUIExpressions->setFitAllColumnsToWidget( true );
mUIExpressions->setMainColumn( 1 );
mUIConsole->setLocked( true );
@@ -333,7 +598,7 @@ void StatusDebuggerController::createContainer() {
mUIConsole->setShowLineNumber( false );
mUIConsole->getDocument().reset();
mUIConsole->setScrollY( mUIConsole->getMaxScroll().y );
mUIConsole->on( Event::OnScrollChange, [this]( auto ) {
mEventConnections += mUIConsole->connect( Event::OnScrollChange, [this]( const Event* ) {
mScrollLocked = mUIConsole->getMaxScroll().y == mUIConsole->getScroll().y;
} );
}
@@ -3,8 +3,11 @@
#include "../../uistatusbar.hpp"
#include "models/breakpointsmodel.hpp"
#include <eepp/core/containers.hpp>
#include <eepp/scene/eventconnection.hpp>
#include <eepp/system/luapattern.hpp>
#include <eepp/ui/tools/uicodeeditorsplitter.hpp>
#include <eepp/ui/tools/uitabwidgetsplitter.hpp>
#include <eepp/ui/uicodeeditor.hpp>
#include <eepp/ui/uirelativelayout.hpp>
#include <eepp/ui/uiscenenode.hpp>
@@ -34,7 +37,7 @@ class UIBreakpointsTableView : public UITableView {
UIWidget* createCell( UIWidget* rowWidget, const ModelIndex& index );
};
class StatusDebuggerController : public StatusBarElement {
class StatusDebuggerController : public StatusBarElement, public UITabWidgetSplitter::Client {
public:
enum class State { NotStarted, Running, Paused };
@@ -43,7 +46,11 @@ class StatusDebuggerController : public StatusBarElement {
StatusDebuggerController( UISplitter* mainSplitter, UISceneNode* uiSceneNode,
PluginContextProvider* pluginContext );
virtual ~StatusDebuggerController() {};
virtual ~StatusDebuggerController();
virtual void show();
virtual void hide();
UIWidget* getWidget();
@@ -71,8 +78,14 @@ class StatusDebuggerController : public StatusBarElement {
void setDebuggingState( State state );
const std::string& saveLayout();
std::function<void( StatusDebuggerController*, UIWidget* )> onWidgetCreated{ nullptr };
void onTabCreated( UITab* tab, UIWidget* widget );
void onWidgetFocusChange( UIWidget* widget );
protected:
UIHLinearLayoutCommandExecuter* mContainer{ nullptr };
UITableView* mUIThreads{ nullptr };
@@ -90,9 +103,32 @@ class StatusDebuggerController : public StatusBarElement {
UIPushButton* mUIButStepOver{ nullptr };
UIPushButton* mUIButStepOut{ nullptr };
UITabWidget* mUITabWidget{ nullptr };
UITabWidget* mUIRightTabWidget{ nullptr };
UITabWidgetSplitter* mTabWidgetSplitter{ nullptr };
UILayout* mDebuggerTabsContainer{ nullptr };
UILayout* mRightPanelContainer{ nullptr };
Scene::EventConnectionList mEventConnections;
UnorderedMap<UITabWidget*, Scene::EventConnectionList> mTabWidgetEventConnections;
bool mScrollLocked{ true };
bool mRestoringLayout{ false };
bool mRightPanelDropPreview{ false };
std::string mSerializedLayout;
void createContainer();
void createTabWidgets();
void restoreTabLayout();
void saveTabLayout();
bool rightPanelHasTabs() const;
void beginRightPanelDropPreview();
void endRightPanelDropPreview();
void updateRightPanel();
};
} // namespace ecode
@@ -19,6 +19,7 @@ class Font;
namespace UI {
class UISplitter;
class UILayout;
class UITabWidget;
class UISceneNode;
class UICodeEditor;
@@ -58,6 +59,8 @@ class ProjectDirectoryTree;
struct TerminalConfig;
class UIMainLayout;
class UITreeViewFS;
class StatusDebuggerController;
class UIRightPanel;
class PluginContextProvider {
public:
@@ -65,6 +68,10 @@ class PluginContextProvider {
virtual UISplitter* getMainSplitter() const = 0;
virtual UIRightPanel* getRightPanel() const = 0;
virtual StatusDebuggerController* getStatusDebuggerController() const = 0;
virtual UITreeViewFS* getProjectTreeView() const = 0;
virtual void hideGlobalSearchBar() = 0;
+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