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