diff --git a/include/eepp/ui/uitabwidget.hpp b/include/eepp/ui/uitabwidget.hpp index bdedef7d5..05559f03a 100644 --- a/include/eepp/ui/uitabwidget.hpp +++ b/include/eepp/ui/uitabwidget.hpp @@ -185,6 +185,8 @@ class EE_API UITabWidget : public UIWidget { UIScrollBar* getTabScroll() const; + void swapTabs( UITab* left, UITab* right ); + protected: friend class UITab; @@ -243,8 +245,6 @@ class EE_API UITabWidget : public UIWidget { void tryCloseTab( UITab* tab, FocusTabBehavior focustTabBehavior = FocusTabBehavior::Default ); - void swapTabs( UITab* left, UITab* right ); - void updateScrollBar(); void updateScroll( bool updateFocus = false ); diff --git a/src/eepp/ui/uitabwidget.cpp b/src/eepp/ui/uitabwidget.cpp index b779997b9..3435cd448 100644 --- a/src/eepp/ui/uitabwidget.cpp +++ b/src/eepp/ui/uitabwidget.cpp @@ -915,6 +915,8 @@ void UITabWidget::tryCloseTab( UITab* tab, FocusTabBehavior focusTabBehavior ) { } void UITabWidget::swapTabs( UITab* left, UITab* right ) { + if ( !left || !right ) + return; Uint32 leftIndex = getTabIndex( left ); Uint32 rightIndex = getTabIndex( right ); if ( leftIndex != eeINDEX_NOT_FOUND && rightIndex != eeINDEX_NOT_FOUND ) { diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 290fba803..9584df584 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -3578,7 +3578,8 @@ void App::init( const LogLevel& logLevel, std::string file, const Float& pidelDe } std::string panelUI( String::format( R"css( - #project_view > treeview::row > treeview::cell > treeview::cell::text { + #panel treeview > treeview::row > treeview::cell > treeview::cell::text, + #panel treeview > treeview::row > table::cell > table::cell::text { font-size: %s; } )css", diff --git a/src/tools/ecode/iconmanager.cpp b/src/tools/ecode/iconmanager.cpp index 2daed3424..75f44c599 100644 --- a/src/tools/ecode/iconmanager.cpp +++ b/src/tools/ecode/iconmanager.cpp @@ -202,7 +202,11 @@ void IconManager::init( UISceneNode* sceneNode, FontTrueType* iconFont, FontTrue { "warning", 0xea6c }, { "error", 0xea87 }, { "search-fuzzy", 0xec0d }, - { "source-control", 0xea68 } }; + { "source-control", 0xea68 }, + { "repo", 0xea62 }, + { "repo-pull", 0xeb40 }, + { "repo-push", 0xeb41 }, + { "tag", 0xea66 } }; for ( const auto& icon : codIcons ) iconTheme->add( UIGlyphIcon::New( icon.first, codIconFont, icon.second ) ); diff --git a/src/tools/ecode/notificationcenter.cpp b/src/tools/ecode/notificationcenter.cpp index c7abaa15b..fb3d9f719 100644 --- a/src/tools/ecode/notificationcenter.cpp +++ b/src/tools/ecode/notificationcenter.cpp @@ -14,10 +14,11 @@ NotificationCenter::NotificationCenter( UILayout* layout, PluginManager* pluginM addNotification( sm.message, Seconds( 10 ) ); } else if ( msg.type == PluginMessageType::ShowDocument ) { auto sd = msg.asShowDocument(); - if ( !sd.uri.empty() ) + if ( !sd.uri.empty() ) { addShowRequest( sd.uri.toString(), mLayout->getUISceneNode()->i18n( "open", "Open" ), Seconds( 10 ) ); + } } return {}; } ); diff --git a/src/tools/ecode/plugins/git/git.cpp b/src/tools/ecode/plugins/git/git.cpp index afd844fa6..39bd1f75d 100644 --- a/src/tools/ecode/plugins/git/git.cpp +++ b/src/tools/ecode/plugins/git/git.cpp @@ -143,7 +143,7 @@ Git::CheckoutResult Git::checkout( const std::string& branch, res.returnCode = retCode; res.error = buf; } else { - res.branch = buf; + res.branch = branch; } return res; } @@ -220,6 +220,10 @@ std::vector Git::getAllBranchesAndTags( RefType ref, const std::str } } ); + std::sort( branches.begin(), branches.end(), []( const Branch& left, const Branch& right ) { + return left.type < right.type || left.name < right.name; + } ); + return branches; } diff --git a/src/tools/ecode/plugins/git/gitplugin.cpp b/src/tools/ecode/plugins/git/gitplugin.cpp index a55fd1e34..8a4708dd8 100644 --- a/src/tools/ecode/plugins/git/gitplugin.cpp +++ b/src/tools/ecode/plugins/git/gitplugin.cpp @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -21,16 +23,25 @@ using json = nlohmann::json; namespace ecode { static const char* GIT_EMPTY = ""; -static const char* GIT_SUCCESS = "theme-success"; -static const char* GIT_ERROR = "theme-error"; +static const char* GIT_SUCCESS = "success"; +static const char* GIT_ERROR = "error"; static const char* GIT_BOLD = "bold"; static const char* GIT_NOT_BOLD = "notbold"; +static const std::string GIT_TAG = "tag"; +static const std::string GIT_REPO = "repo"; + +static size_t hashBranches( const std::vector& branches ) { + size_t hash = 0; + for ( const auto& branch : branches ) + hash = hashCombine( hash, String::hash( branch.name ), String::hash( branch.lastCommit ) ); + return hash; +} class GitBranchModel : public Model { public: static std::shared_ptr asModel( std::vector&& branches, - GitPlugin* gitPlugin ) { - return std::make_shared( std::move( branches ), gitPlugin ); + size_t hash, GitPlugin* gitPlugin ) { + return std::make_shared( std::move( branches ), hash, gitPlugin ); } enum Column { Name, Remote, Type, LastCommit }; @@ -54,8 +65,8 @@ class GitBranchModel : public Model { return ""; } - GitBranchModel( std::vector&& branches, GitPlugin* gitPlugin ) : - mPlugin( gitPlugin ) { + GitBranchModel( std::vector&& branches, size_t hash, GitPlugin* gitPlugin ) : + mPlugin( gitPlugin ), mHash( hash ) { std::map> branchTypes; for ( auto& branch : branches ) { auto& type = branchTypes[refTypeToString( branch.type )]; @@ -96,6 +107,17 @@ class GitBranchModel : public Model { return {}; } + UIIcon* iconFor( const ModelIndex& index ) const { + if ( index.column() == (Int64)treeColumn() ) { + if ( index.hasParent() ) { + Git::Branch* branch = static_cast( index.internalData() ); + return mPlugin->getUISceneNode()->findIcon( + branch->type == Git::RefType::Tag ? GIT_TAG : GIT_REPO ); + } + } + return nullptr; + } + Variant data( const ModelIndex& index, ModelRole role ) const { switch ( role ) { case ModelRole::Display: { @@ -107,6 +129,10 @@ class GitBranchModel : public Model { const Git::Branch& branch = mBranches[index.internalId()].data[index.row()]; switch ( index.column() ) { case Column::Name: + if ( branch.type == Git::Remote && + String::startsWith( branch.name, "origin/" ) ) { + return Variant( std::string_view{ branch.name }.substr( 7 ).data() ); + } return Variant( branch.name.c_str() ); case Column::Remote: return Variant( branch.remote.c_str() ); @@ -125,6 +151,9 @@ class GitBranchModel : public Model { return Variant( GIT_BOLD ); return Variant( GIT_NOT_BOLD ); } + case ModelRole::Icon: { + return iconFor( index ); + } default: break; } @@ -133,15 +162,19 @@ class GitBranchModel : public Model { virtual bool classModelRoleEnabled() { return true; } + size_t getHash() const { return mHash; } + protected: std::vector mBranches; - GitPlugin* mPlugin; + GitPlugin* mPlugin{ nullptr }; + size_t mHash{ 0 }; }; class GitStatusModel : public Model { public: - static std::shared_ptr asModel( Git::FilesStatus&& status ) { - return std::make_shared( std::move( status ) ); + static std::shared_ptr asModel( Git::FilesStatus status, + GitPlugin* gitPlugin ) { + return std::make_shared( std::move( status ), gitPlugin ); } struct RepoStatus { @@ -149,9 +182,9 @@ class GitStatusModel : public Model { std::vector files; }; - enum Column { File, Inserted, Removed, FileStatus }; + enum Column { File, State, Inserted, Removed, RelativeDirectory }; - GitStatusModel( Git::FilesStatus&& status ) { + GitStatusModel( Git::FilesStatus&& status, GitPlugin* gitPlugin ) : mPlugin( gitPlugin ) { mStatus.reserve( status.size() ); for ( auto& s : status ) mStatus.emplace_back( RepoStatus{ std::move( s.first ), std::move( s.second ) } ); @@ -167,7 +200,7 @@ class GitStatusModel : public Model { return 0; } - size_t columnCount( const ModelIndex& ) const { return 4; } + size_t columnCount( const ModelIndex& ) const { return 5; } ModelIndex parentIndex( const ModelIndex& index ) const { if ( !index.isValid() || index.internalId() == -1 ) @@ -186,40 +219,52 @@ class GitStatusModel : public Model { } Variant data( const ModelIndex& index, ModelRole role ) const { - if ( role == ModelRole::Display ) { - if ( index.internalId() == -1 ) { - if ( index.column() == Column::File ) - return Variant( mStatus[index.row()].repo.c_str() ); - return Variant( GIT_EMPTY ); + switch ( role ) { + case ModelRole::Display: { + if ( index.internalId() == -1 ) { + if ( index.column() == Column::File ) + return Variant( mStatus[index.row()].repo.c_str() ); + return Variant( GIT_EMPTY ); + } + const Git::DiffFile& s = mStatus[index.internalId()].files[index.row()]; + switch ( index.column() ) { + case Column::File: + return Variant( FileSystem::fileNameFromPath( s.file ) ); + case Column::Inserted: + return Variant( String::format( " +%d ", s.inserts ) ); + case Column::Removed: + return Variant( String::format( " -%d ", s.deletes ) ); + case Column::State: + return Variant( String::format( " %c ", s.status ) ); + case Column::RelativeDirectory: + return Variant( FileSystem::fileRemoveFileName( s.file ) ); + } + break; } - const Git::DiffFile& s = mStatus[index.internalId()].files[index.row()]; - switch ( index.column() ) { - case Column::File: - return Variant( s.file.c_str() ); - case Column::Inserted: - return Variant( s.inserts ); - case Column::Removed: - return Variant( s.deletes ); - case Column::FileStatus: - return Variant( std::string( static_cast( s.status ), 1 ) ); - } - } else if ( role == ModelRole::Class ) { - switch ( index.column() ) { - case Column::Inserted: - return Variant( GIT_SUCCESS ); - case Column::Removed: - return Variant( GIT_ERROR ); - default: - break; + case ModelRole::Class: { + if ( index.internalId() != -1 ) { + switch ( index.column() ) { + case Column::Inserted: + return Variant( GIT_SUCCESS ); + case Column::Removed: + return Variant( GIT_ERROR ); + default: + break; + } + } + break; } + default: + break; } - return Variant( GIT_EMPTY ); + return {}; } virtual bool classModelRoleEnabled() { return true; } protected: std::vector mStatus; + GitPlugin* mPlugin{ nullptr }; }; Plugin* GitPlugin::New( PluginManager* pluginManager ) { @@ -343,8 +388,8 @@ void GitPlugin::load( PluginManager* pluginManager ) { mGitFound = !mGit->getGitPath().empty(); if ( getUISceneNode() ) { - updateStatusBar(); - // updateBranches(); + updateStatus(); + updateBranches(); } subscribeFileSystemListener(); @@ -357,8 +402,8 @@ void GitPlugin::updateUINow( bool force ) { if ( !mGit || !getUISceneNode() ) return; - updateStatusBar( force ); - // updateBranches(); + updateStatus( force ); + updateBranches(); } void GitPlugin::updateUI() { @@ -370,6 +415,18 @@ void GitPlugin::updateUI() { } void GitPlugin::updateStatusBarSync() { + buildSidePanelTab(); + + mGitContentView->setVisible( !mGit->getGitFolder().empty() ) + ->setEnabled( !mGit->getGitFolder().empty() ); + mGitNoContentView->setVisible( !mGitContentView->isVisible() ) + ->setEnabled( !mGitContentView->isEnabled() ); + + if ( !mGit->getGitFolder().empty() ) { + mStatusTree->setModel( GitStatusModel::asModel( mGitStatus.files, this ) ); + mStatusTree->expandAll(); + } + if ( !mStatusBar ) getUISceneNode()->bind( "status_bar", mStatusBar ); if ( !mStatusBar ) @@ -390,6 +447,11 @@ void GitPlugin::updateStatusBarSync() { auto childCount = mStatusBar->getChildCount(); if ( childCount > 2 ) mStatusButton->toPosition( mStatusBar->getChildCount() - 2 ); + + mStatusButton->on( Event::MouseClick, [this]( const Event* ) { + if ( mTab ) + mTab->setTabSelected(); + } ); } mStatusButton->setVisible( !mGit->getGitFolder().empty() ); @@ -430,7 +492,7 @@ void GitPlugin::updateStatusBarSync() { mStatusButton->invalidateDraw(); } -void GitPlugin::updateStatusBar( bool force ) { +void GitPlugin::updateStatus( bool force ) { if ( !mGit || !mGitFound || !mStatusBarDisplayBranch ) return; mThreadPool->run( [this, force] { @@ -637,6 +699,10 @@ void GitPlugin::onRegister( UICodeEditor* editor ) { doc.setCommand( "git-blame", [this]( TextDocument::Client* client ) { blame( static_cast( client ) ); } ); + doc.setCommand( "show-source-control-tab", [this]() { + if ( mTab ) + mTab->setTabSelected(); + } ); } void GitPlugin::onUnregister( UICodeEditor* editor ) { @@ -696,18 +762,21 @@ void GitPlugin::updateBranches() { if ( mGitBranch.empty() ) mGitBranch = mGit->branch(); auto branches = mGit->getAllBranchesAndTags(); - auto model = GitBranchModel::asModel( std::move( branches ), this ); - getUISceneNode()->runOnMainThread( [this, model] { updateSidePanelTab( model ); } ); + auto hash = hashBranches( branches ); + auto model = GitBranchModel::asModel( std::move( branches ), hash, this ); + if ( mBranchesTree && + static_cast( mBranchesTree->getModel() )->getHash() == hash ) + return; + getUISceneNode()->runOnMainThread( [this, model] { updateBranchesUI( model ); } ); } } ); } -void GitPlugin::updateSidePanelTab( std::shared_ptr model ) { +void GitPlugin::updateBranchesUI( std::shared_ptr model ) { buildSidePanelTab(); - - UITreeView* tree = mSidePanel->find( "git_branches_tree" ); - tree->setModel( model ); - tree->setColumnsVisible( { GitBranchModel::Name } ); + mBranchesTree->setModel( model ); + mBranchesTree->setColumnsVisible( { GitBranchModel::Name } ); + mBranchesTree->expandAll(); } void GitPlugin::buildSidePanelTab() { @@ -718,22 +787,84 @@ void GitPlugin::buildSidePanelTab() { UIIcon* icon = getUISceneNode()->findIcon( "source-control" ); UIWidget* node = getUISceneNode()->loadLayoutFromString( R"html( - - - + + + + + + + + + + + + + + )html" ); mTab = mSidePanel->add( getUISceneNode()->i18n( "source_control", "Source Control" ), node, icon ? icon->getSize( PixelDensity::dpToPx( 12 ) ) : nullptr ); mTab->setId( "source_control" ); mTab->setTextAsFallback( true ); - UITreeView* tree = mSidePanel->find( "git_branches_tree" ); - tree->setAutoExpandOnSingleColumn( true ); - tree->setHeadersVisible( false ); + node->bind( "git_panel_switcher", mPanelSwicher ); + node->bind( "git_panel_stack", mStackWidget ); + node->bind( "git_branches_tree", mBranchesTree ); + node->bind( "git_status_tree", mStatusTree ); + node->bind( "git_content", mGitContentView ); + node->bind( "git_no_content", mGitNoContentView ); + + mBranchesTree->setAutoExpandOnSingleColumn( true ); + mBranchesTree->setHeadersVisible( false ); + mBranchesTree->on( Event::OnModelEvent, [this]( const Event* event ) { + const ModelEvent* modelEvent = static_cast( event ); + if ( !modelEvent->getModelIndex().hasParent() ) + return; + + const Git::Branch* branch = + static_cast( modelEvent->getModelIndex().internalData() ); + + switch ( modelEvent->getModelEventType() ) { + case EE::UI::Abstract::ModelEventType::Open: { + auto result = mGit->checkout( branch->name ); + if ( result.returnCode == EXIT_SUCCESS ) { + mGitBranch = branch->name; + if ( mBranchesTree->getModel() ) + mBranchesTree->getModel()->invalidate( Model::DontInvalidateIndexes ); + } else { + showMessage( LSPMessageType::Warning, result.error ); + } + break; + } + case EE::UI::Abstract::ModelEventType::OpenTree: + case EE::UI::Abstract::ModelEventType::CloseTree: + case EE::UI::Abstract::ModelEventType::OpenMenu: + break; + } + } ); + + auto listBox = mPanelSwicher->getListBox(); + listBox->addListBoxItems( { i18n( "branches", "Branches" ), i18n( "status", "Status" ) } ); + mStackMap.resize( 2 ); + mStackMap[0] = node->find( "git_branches" ); + mStackMap[1] = node->find( "git_status" ); + listBox->setSelected( 0 ); + + mPanelSwicher->addEventListener( Event::OnItemSelected, [this, listBox]( const Event* ) { + mStackWidget->setActiveWidget( mStackMap[listBox->getItemSelectedIndex()] ); + } ); + + mStatusTree->setAutoColumnsWidth( true ); + mStatusTree->setHeadersVisible( false ); } } // namespace ecode diff --git a/src/tools/ecode/plugins/git/gitplugin.hpp b/src/tools/ecode/plugins/git/gitplugin.hpp index 22e9f597f..fa297f081 100644 --- a/src/tools/ecode/plugins/git/gitplugin.hpp +++ b/src/tools/ecode/plugins/git/gitplugin.hpp @@ -11,6 +11,13 @@ using namespace EE::UI::Models; using namespace EE::UI; +namespace EE::UI { +class UITreeView; +class UIDropDownList; +class UIStackWidget; +class UIListBoxItem; +} + namespace ecode { class Git; @@ -86,6 +93,14 @@ class GitPlugin : public PluginBase { UITabWidget* mSidePanel{ nullptr }; UITab* mTab{ nullptr }; + UITreeView* mBranchesTree{ nullptr }; + UITreeView* mStatusTree{ nullptr }; + UIDropDownList* mPanelSwicher{ nullptr }; + UIStackWidget* mStackWidget{ nullptr }; + std::vector mStackMap; + UIWidget* mGitContentView{ nullptr }; + UIWidget* mGitNoContentView{ nullptr }; + struct CustomTokenizer { SyntaxDefinition def; SyntaxColorScheme scheme; @@ -97,7 +112,7 @@ class GitPlugin : public PluginBase { void blame( UICodeEditor* editor ); - void updateStatusBar( bool force = false ); + void updateStatus( bool force = false ); void updateStatusBarSync(); @@ -109,7 +124,7 @@ class GitPlugin : public PluginBase { void buildSidePanelTab(); - void updateSidePanelTab( std::shared_ptr ); + void updateBranchesUI( std::shared_ptr ); }; } // namespace ecode diff --git a/src/tools/ecode/plugins/plugin.cpp b/src/tools/ecode/plugins/plugin.cpp index e0b0b583f..893597076 100644 --- a/src/tools/ecode/plugins/plugin.cpp +++ b/src/tools/ecode/plugins/plugin.cpp @@ -53,6 +53,15 @@ String Plugin::i18n( const std::string& key, const String& def ) const { return getManager()->getUISceneNode()->i18n( key, def ); } +void Plugin::showMessage( LSPMessageType type, const std::string& message, + const std::string& title ) { + if ( !mManager ) + return; + LSPShowMessageParams msgReq{ type, message, { { title } } }; + mManager->sendBroadcast( PluginMessageType::ShowMessage, PluginMessageFormat::ShowMessage, + &msgReq ); +} + void Plugin::onFileSystemEvent( const FileEvent& ev, const FileInfo& file ) { if ( ev.type != FileSystemEventType::Modified || mShuttingDown || isLoading() ) return; diff --git a/src/tools/ecode/plugins/plugin.hpp b/src/tools/ecode/plugins/plugin.hpp index c3385efd3..79ddad622 100644 --- a/src/tools/ecode/plugins/plugin.hpp +++ b/src/tools/ecode/plugins/plugin.hpp @@ -1,6 +1,7 @@ #ifndef ECODE_PLUGIN_HPP #define ECODE_PLUGIN_HPP +#include "lsp/lspprotocol.hpp" #include #include @@ -40,7 +41,10 @@ class Plugin : public UICodeEditorPlugin { String i18n( const std::string& key, const String& def ) const; - virtual void onVersionUpgrade( Uint32 oldVersion, Uint32 currentVersion ) {} + virtual void onVersionUpgrade( Uint32 /*oldVersion*/, Uint32 /*currentVersion*/ ) {} + + void showMessage( LSPMessageType type, const std::string& message, + const std::string& title = "" ); protected: PluginManager* mManager{ nullptr }; diff --git a/src/tools/ecode/projectbuild.cpp b/src/tools/ecode/projectbuild.cpp index d34aeac64..60a4aa100 100644 --- a/src/tools/ecode/projectbuild.cpp +++ b/src/tools/ecode/projectbuild.cpp @@ -852,6 +852,13 @@ void ProjectBuildManager::buildSidePanelTab() { mTab->setId( "build_tab" ); mTab->setTextAsFallback( true ); + auto tabIndex = mSidePanel->getTabIndex( mTab ); + if ( tabIndex > 0 ) { + auto prevTab = mSidePanel->getTab( tabIndex - 1 ); + if ( prevTab && prevTab->getId() != "treeview_tab" ) + mSidePanel->swapTabs( mTab, prevTab ); + } + updateSidePanelTab(); }