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
+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