From 61d9d3c7bc00d499a83f100ada5fe153d1e5cd6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 29 Aug 2026 02:15:26 -0300 Subject: [PATCH] Improve Git History navigation and commit details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a detached Git Commit History tab with a traditional table layout, persistent column widths, commit focusing, and a splitter-based commit details pane. Register the git-show-history command and expose “Show in Git History” from commit details and history context menus. Add History and Git Commit tab icons, localized titles, appropriate tooltips, and ref-aware tab naming. Share the commit-details layout and behavior between standalone commit tabs and detached History views. Move view creation, widget binding, state management, controls, and asynchronous changed-file loading into CommitDetailsState. Display complete commit metadata and messages, GitHub links, copyable commit SHAs, colored changed-file statistics, and multi-file diffs with global expand/collapse and unified/split controls. Improve detached-history lifecycle behavior, selection and focus handling, stale-request protection, tooltip formatting, and column recalculation. Restore “Load older commits” by preserving pagination-node semantics while asynchronous loading is in progress. Fix expandable History rows so merge subjects and metadata render correctly without regressing generic UITreeView cells. Add recursive commit lookup for focusing nested history entries. Add deferred tooltip-created notifications and configure History tooltips only after their widgets exist. Preserve tokenizer-applied UITextView color ranges during layout recalculation when custom styling is enabled, preventing changed-file statistics from losing their colors after splitter resizing. Add the required Codicons and translations, expose UIWidget pseudo-class queries, reuse cached tree-expander drawable sources, and use SmallVector for nested stylesheet property resolution. --- bin/assets/i18n/de.xml | 6 + bin/assets/i18n/en.xml | 6 + bin/assets/i18n/fr.xml | 6 + bin/assets/i18n/zh.xml | 6 + include/eepp/ui/uistyle.hpp | 2 +- include/eepp/ui/uiwidget.hpp | 11 + src/eepp/ui/iconmanager.cpp | 2 + src/eepp/ui/uitextview.cpp | 5 + src/eepp/ui/uitreeview.cpp | 2 +- src/eepp/ui/uiwidget.cpp | 4 + .../ecode/plugins/git/githistorymodel.cpp | 17 +- .../ecode/plugins/git/githistorymodel.hpp | 6 +- .../ecode/plugins/git/githistorytreeview.cpp | 4 +- src/tools/ecode/plugins/git/gitplugin.cpp | 573 ++++++++++++------ src/tools/ecode/plugins/git/gitplugin.hpp | 105 +++- 15 files changed, 551 insertions(+), 204 deletions(-) diff --git a/bin/assets/i18n/de.xml b/bin/assets/i18n/de.xml index 98ef84fdc..222214b09 100644 --- a/bin/assets/i18n/de.xml +++ b/bin/assets/i18n/de.xml @@ -1264,6 +1264,12 @@ Für sichtbare Änderung ecode neu starten. Ältere Commits laden Noch keine Commits Keine weiteren Commits + Im Git-Verlauf anzeigen + Git-Commit-Verlauf – %s + Commit-SHA + Commit-SHA kopieren %s + Zur geteilten Diff-Ansicht wechseln + Zur vereinheitlichten Diff-Ansicht wechseln Git-Verlauf konnte nicht geladen werden Wiederholen Lokal diff --git a/bin/assets/i18n/en.xml b/bin/assets/i18n/en.xml index d4207b7e3..8c203c154 100644 --- a/bin/assets/i18n/en.xml +++ b/bin/assets/i18n/en.xml @@ -1249,6 +1249,12 @@ Restart ecode to see the changes. Load older commits No commits yet No additional commits + Show in Git History + Git Commit History - %s + Commit SHA + Copy Commit SHA %s + Switch to split diff view + Switch to unified diff view Could not load Git history Retry Local diff --git a/bin/assets/i18n/fr.xml b/bin/assets/i18n/fr.xml index 8655312ca..55582671e 100644 --- a/bin/assets/i18n/fr.xml +++ b/bin/assets/i18n/fr.xml @@ -1248,6 +1248,12 @@ Redémarrer ecode pour voir les changements. Charger les commits précédents Aucun commit pour le moment Aucun commit supplémentaire + Afficher dans l’historique Git + Historique des commits Git – %s + SHA du commit + Copier le SHA du commit %s + Passer à la vue des différences séparée + Passer à la vue des différences unifiée Impossible de charger l’historique Git Réessayer Locale diff --git a/bin/assets/i18n/zh.xml b/bin/assets/i18n/zh.xml index 5f7812978..3e815aa44 100644 --- a/bin/assets/i18n/zh.xml +++ b/bin/assets/i18n/zh.xml @@ -1053,6 +1053,12 @@ file in the directory tree. 加载更早的提交 尚无提交 没有其他提交 + 在 Git 历史记录中显示 + Git 提交历史记录 - %s + 提交 SHA + 复制提交 SHA %s + 切换到拆分差异视图 + 切换到统一差异视图 无法加载 Git 历史记录 重试 本地 diff --git a/include/eepp/ui/uistyle.hpp b/include/eepp/ui/uistyle.hpp index d181ad6be..2476c0aa9 100644 --- a/include/eepp/ui/uistyle.hpp +++ b/include/eepp/ui/uistyle.hpp @@ -141,7 +141,7 @@ class EE_API UIStyle : public UIState { /** Lazily allocated for styles that use substitutions. The common, non-reentrant resolution * needs no container allocation; nested slots are retained for later reuse. */ std::unique_ptr mPropertyResolutionSlot; - std::vector> mNestedPropertyResolutionSlots; + SmallVector, 2> mNestedPropertyResolutionSlots; bool mChangingState; bool mForceReapplyProperties; bool mDisableAnimations; diff --git a/include/eepp/ui/uiwidget.hpp b/include/eepp/ui/uiwidget.hpp index 84db6474c..1fabedec0 100644 --- a/include/eepp/ui/uiwidget.hpp +++ b/include/eepp/ui/uiwidget.hpp @@ -1342,6 +1342,17 @@ class EE_API UIWidget : public UINode { */ bool hasPseudoClass( const std::string& pseudoCls ) const; + /** + * @brief Checks if this widget has a specific pseudo-class. + * + * Determines whether this widget currently has the specified pseudo-class + * (e.g., hover, focus, active). + * + * @param pseudoCls The pseudo-class to check. + * @return True if the widget has the pseudo-class, false otherwise. + */ + bool hasPseudoClass( StyleSheetSelectorRule::PseudoClasses pseudoCls ) const; + /** * @brief Checks if the tooltip is enabled for this widget. * diff --git a/src/eepp/ui/iconmanager.cpp b/src/eepp/ui/iconmanager.cpp index 10653ca9f..5f721603d 100644 --- a/src/eepp/ui/iconmanager.cpp +++ b/src/eepp/ui/iconmanager.cpp @@ -261,6 +261,8 @@ ResourcePtr IconManager::init( const std::string& iconThemeName, { "error", 0xea87 }, { "search-fuzzy", 0xec0d }, { "source-control", 0xea68 }, + { "history", 0xea82 }, + { "git-commit", 0xeafc }, { "repo", 0xea62 }, { "repo-pull", 0xeb40 }, { "repo-push", 0xeb41 }, diff --git a/src/eepp/ui/uitextview.cpp b/src/eepp/ui/uitextview.cpp index 5cf4a0eac..408f67a55 100644 --- a/src/eepp/ui/uitextview.cpp +++ b/src/eepp/ui/uitextview.cpp @@ -735,6 +735,11 @@ void UITextView::onAlignChange() { } void UITextView::onSelectionChange() { + if ( mUsingCustomStyling ) { + invalidateDraw(); + return; + } + mTextCache.invalidateColors(); if ( selCurInit() != selCurEnd() ) { diff --git a/src/eepp/ui/uitreeview.cpp b/src/eepp/ui/uitreeview.cpp index 730ab9890..bce885270 100644 --- a/src/eepp/ui/uitreeview.cpp +++ b/src/eepp/ui/uitreeview.cpp @@ -308,7 +308,7 @@ UIWidget* UITreeView::updateCell( const Vector2& posIndex, const ModelInd if ( hasChildren ) { UIIcon* icon = getIndexMetadata( index ).open ? mExpandIcon : mContractIcon; DrawablePtr drawable = - icon ? icon->createDrawable( mExpanderIconSize ) : DrawablePtr{}; + icon ? icon->getSource( mExpanderIconSize ) : DrawablePtr{}; if ( drawable == nullptr ) { image->setVisible( false ); diff --git a/src/eepp/ui/uiwidget.cpp b/src/eepp/ui/uiwidget.cpp index 6f08fed0e..06d362a4d 100644 --- a/src/eepp/ui/uiwidget.cpp +++ b/src/eepp/ui/uiwidget.cpp @@ -1333,6 +1333,10 @@ bool UIWidget::hasPseudoClass( const std::string& pseudoCls ) const { return ( mPseudoClasses & StyleSheetSelectorRule::toPseudoClass( pseudoCls ) ) != 0; } +bool UIWidget::hasPseudoClass( StyleSheetSelectorRule::PseudoClasses pseudoCls ) const { + return ( mPseudoClasses & static_cast( pseudoCls ) ) != 0; +} + bool UIWidget::isTooltipEnabled() const { return ( mFlags & UI_TOOLTIP_ENABLED ) != 0; } diff --git a/src/tools/ecode/plugins/git/githistorymodel.cpp b/src/tools/ecode/plugins/git/githistorymodel.cpp index 3a7c1dfe2..44025547a 100644 --- a/src/tools/ecode/plugins/git/githistorymodel.cpp +++ b/src/tools/ecode/plugins/git/githistorymodel.cpp @@ -2,6 +2,7 @@ #include "gitplugin.hpp" #include #include +#include #include namespace ecode { @@ -49,6 +50,19 @@ ModelIndex GitHistoryModel::indexForNode( const Node* item, int column ) const { return {}; } +ModelIndex GitHistoryModel::indexForCommit( std::string_view hash, int column ) const { + std::function find = [&]( const Nodes& nodes ) -> const Node* { + for ( const auto& item : nodes ) { + if ( item->type == NodeType::Commit && item->commit.hash == hash ) + return item.get(); + if ( const Node* found = find( item->children ) ) + return found; + } + return nullptr; + }; + return indexForNode( find( mRoots ), column ); +} + ModelIndex GitHistoryModel::parentIndex( const ModelIndex& index ) const { Node* item = node( index ); return item ? indexForNode( item->parent ) : ModelIndex{}; @@ -241,7 +255,8 @@ void GitHistoryModel::setPageLoading( Node* item ) { if ( !item || item->childrenLoading ) return; item->childrenLoading = true; - item->type = NodeType::Loading; + // Keep the semantic node type intact: appendPage() must still identify this row as the + // pagination cursor when the asynchronous request completes. item->message = mPlugin->i18n( "git_history_loading", "Loading..." ); item->error.clear(); invalidate( Model::DontInvalidateIndexes ); diff --git a/src/tools/ecode/plugins/git/githistorymodel.hpp b/src/tools/ecode/plugins/git/githistorymodel.hpp index 5f01bc28b..277d908bb 100644 --- a/src/tools/ecode/plugins/git/githistorymodel.hpp +++ b/src/tools/ecode/plugins/git/githistorymodel.hpp @@ -14,7 +14,7 @@ class GitPlugin; class GitHistoryModel : public Model { public: enum class NodeType : uint8_t { Commit, LoadMore, Loading, Error, Empty }; - enum Column { Subject, Author, Date, Hash }; + enum Column { Subject, Date, Author, Hash }; struct Node { Git::Commit commit; Git::HistoryQuery query; @@ -43,7 +43,7 @@ class GitHistoryModel : public Model { size_t rowCount( const ModelIndex& index = {} ) const; - size_t columnCount( const ModelIndex& = {} ) const { return 1; } + size_t columnCount( const ModelIndex& = {} ) const { return 4; } std::string columnName( const size_t& column ) const; @@ -61,6 +61,8 @@ class GitHistoryModel : public Model { ModelIndex indexForNode( const Node* node, int column = 0 ) const; + ModelIndex indexForCommit( std::string_view hash, int column = Subject ) const; + void setRootLoading(); void setRootPage( Git::HistoryPage page, const Git::HistoryQuery& query ); diff --git a/src/tools/ecode/plugins/git/githistorytreeview.cpp b/src/tools/ecode/plugins/git/githistorytreeview.cpp index ace54c6d5..0e48190e7 100644 --- a/src/tools/ecode/plugins/git/githistorytreeview.cpp +++ b/src/tools/ecode/plugins/git/githistorytreeview.cpp @@ -50,8 +50,8 @@ void GitHistoryTreeViewCell::draw() { if ( mMetadataText.getString().empty() ) return; - const bool selected = getParent() && ( getParent()->asType()->getStyleState() & - UIState::StateFlagSelected ); + const bool selected = getParent() && ( getParent()->asType()->hasPseudoClass( + StyleSheetSelectorRule::PseudoClasses::Selected ) ); mMetadataText.setFillColor( selected ? mTextBox->getFontColor() : mHintColor ); mMetadataText.draw( std::floor( mScreenPos.x + mTextBox->getPixelsPosition().x ), diff --git a/src/tools/ecode/plugins/git/gitplugin.cpp b/src/tools/ecode/plugins/git/gitplugin.cpp index a48fd6f4f..026a6a869 100644 --- a/src/tools/ecode/plugins/git/gitplugin.cpp +++ b/src/tools/ecode/plugins/git/gitplugin.cpp @@ -1,4 +1,5 @@ #include "gitplugin.hpp" +#include "../../appconfig.hpp" #include "../../settingspage.hpp" #include "gitbranchmodel.hpp" #include "githistorymodel.hpp" @@ -22,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -628,6 +630,28 @@ void GitPlugin::updateStatusBarSync() { mStatusButton->invalidateDraw(); } +void GitPlugin::styleCommitFilesStatus( UITextView* status ) { + if ( !status ) + return; + status->setUsingCustomStyling( true ); + if ( !mCommitStatusCustomTokenizer.has_value() ) { + std::vector patterns; + patterns.emplace_back( SyntaxPattern( { ".*%((%d+)%)%s+(%+%d+)%s+(%-%d+)" }, + { "normal", "warning", "keyword", "type" } ) ); + SyntaxDefinition syntaxDef( "git_commit_files_status", {}, std::move( patterns ) ); + SyntaxColorScheme scheme( "git_commit_files_status", + { { "normal"_sst, { getVarColor( "--font" ) } }, + { "warning"_sst, { getVarColor( "--theme-warning" ) } }, + { "keyword"_sst, { getVarColor( "--theme-success" ) } }, + { "type"_sst, { getVarColor( "--theme-error" ) } } }, + {} ); + mCommitStatusCustomTokenizer = { std::move( syntaxDef ), std::move( scheme ) }; + } + SyntaxTokenizer::tokenizeText( mCommitStatusCustomTokenizer->def, + mCommitStatusCustomTokenizer->scheme, status->getTextCache() ); + status->invalidateDraw(); +} + void GitPlugin::updateStatus( bool force ) { if ( !mGit || !mGitFound ) return; @@ -793,6 +817,9 @@ PluginRequestHandle GitPlugin::processMessage( const PluginMessage& msg ) { } case ecode::PluginMessageType::UIThemeReloaded: { mStatusCustomTokenizer.reset(); + mCommitStatusCustomTokenizer.reset(); + styleCommitFilesStatus( mCommitDetails.status ); + styleCommitFilesStatus( mDetachedHistory.details.status ); updateUINow( true ); break; } @@ -2245,6 +2272,7 @@ void GitPlugin::onRegister( UICodeEditor* editor ) { doc.setCommand( "git-push", [this] { push( projectPath() ); } ); doc.setCommand( "git-fetch", [this] { fetch( projectPath() ); } ); doc.setCommand( "git-commit", [this] { commit( projectPath() ); } ); + doc.setCommand( "git-show-history", [this] { showGitHistory(); } ); } void GitPlugin::onUnregister( UICodeEditor* editor ) { @@ -2373,6 +2401,7 @@ void GitPlugin::updateHistoryRefs( const std::shared_ptr& model mHistoryRefDropDown->getListView()->getSelection().set( mHistoryRefModel->index( selected, 0 ) ); mUpdatingHistoryRefs = false; + updateDetachedHistoryTitle(); if ( revisionChanged ) invalidateHistory(); } @@ -2397,6 +2426,8 @@ void GitPlugin::reloadHistory() { mHistoryTree->setColumnsVisible( { GitHistoryModel::Subject } ); } mHistoryTree->clearViewMetadata(); + if ( mDetachedHistory.tree ) + mDetachedHistory.tree->clearViewMetadata(); mHistoryModel->setRootLoading(); Git::HistoryQuery query; query.revision = mHistoryRevision; @@ -2416,6 +2447,7 @@ void GitPlugin::reloadHistory() { plugin->mHistoryModel->setRootPage( std::move( page ), query ); else plugin->mHistoryModel->setRootError( std::move( page.result ) ); + plugin->focusDetachedHistory(); } ); }, [this]( auto ) { --mRunningHistoryRequests; } ); @@ -2460,6 +2492,8 @@ void GitPlugin::loadHistoryPage( GitHistoryModel::Node* node, Git::HistoryQuery else plugin->mHistoryModel->setChildrenPage( node, std::move( page ), query ); plugin->mHistoryTree->recalculateColumnsWidth(); + if ( plugin->mDetachedHistory.tree ) + plugin->mDetachedHistory.tree->recalculateColumnsWidth(); } ); }, [this]( auto ) { --mRunningHistoryRequests; } ); @@ -2491,212 +2525,368 @@ void GitPlugin::activateHistoryIndex( const ModelIndex& index, bool expand ) { } } +void GitPlugin::openHistoryMenu( const ModelIndex& index ) { + if ( !mHistoryModel ) + return; + const auto* node = mHistoryModel->node( index ); + if ( !node || node->type != GitHistoryModel::NodeType::Commit ) + return; + UIPopUpMenu* menu = UIPopUpMenu::New(); + menuAdd( menu, "git-show-history", i18n( "git_show_in_history", "Show in Git History" ), + "history" ); + const Git::Commit commit = node->commit; + menu->on( Event::OnItemClicked, [this, commit]( const Event* event ) { + if ( event->getNode()->asType()->getId() == "git-show-history" ) + showGitHistory( &commit ); + } ); + menu->showOverMouseCursor(); +} + +std::string GitPlugin::detachedHistoryTitle() { + String state = String::fromUtf8( mHistoryRevision ); + if ( mHistoryRefDropDown && mHistoryRefModel ) { + const ModelIndex selected = mHistoryRefDropDown->getListView()->getSelection().first(); + if ( selected.isValid() ) { + Variant display = mHistoryRefModel->data( selected ); + if ( display.is( Variant::Type::StringPtr ) ) + state = display.asStringPtr(); + else if ( display.isValid() ) + state = display.toString(); + } + } + return String::format( i18n( "git_commit_history_title", "Git Commit History - %s" ).toUtf8(), + state.toUtf8() ); +} + +void GitPlugin::updateDetachedHistoryTitle() { + if ( !mDetachedHistory.view ) + return; + auto tabs = mManager->getSplitter()->getTabFromOwnedWidgetId( mDetachedHistory.view->getId() ); + if ( !tabs.empty() ) + tabs.front().first->setText( detachedHistoryTitle() ); +} + +void GitPlugin::showGitHistory( const Git::Commit* commit ) { + if ( !mHistoryTree ) + return; + ensureHistoryLoaded(); + if ( commit ) + mDetachedHistory.focusHash = commit->hash; + else + mDetachedHistory.focusHash.clear(); + + if ( !mDetachedHistory.view || + !mManager->getSplitter()->ownedWidgetExists( mDetachedHistory.view ) ) { + mDetachedHistory.view = getUISceneNode() + ->loadLayoutFromString( R"xml( + + + + + + )xml" ) + ->asType(); + mDetachedHistory.view->setAlwaysShowSplitter( false ); + mDetachedHistory.view->setHideSplitterOnEdge( true ); + mDetachedHistory.view->setSplitPartition( StyleSheetLength( "100%" ) ); + mDetachedHistory.tree = + mDetachedHistory.view->find( "git_history_detached_tree" ); + mDetachedHistory.tree->setModel( mHistoryModel ); + mDetachedHistory.tree->setHeadersVisible( true ); + mDetachedHistory.tree->setRowHeight( PixelDensity::dpToPx( 24 ) ); + mDetachedHistory.tree->setMainColumn( GitHistoryModel::Subject ); + mDetachedHistory.tree->setFitAllColumnsToWidget( true ); + mDetachedHistory.tree->setColumnWidthMode( + UIAbstractTableView::ColumnWidthMode::Percentage ); + mDetachedHistory.tree->setColumnsWidthPercentage( { 0.55f, 0.15f, 0.2f, 0.1f } ); + const std::string savedColumns = getPluginContext()->getConfig().iniState.getValue( + "git", "commit_history_columns", "" ); + if ( !savedColumns.empty() ) { + auto columns = nlohmann::json::parse( savedColumns, nullptr, false ); + if ( !columns.is_discarded() ) + mDetachedHistory.tree->unserializeColumnWidths( columns ); + } + mDetachedHistory.tree->setIndentWidth( PixelDensity::dpToPx( 16 ) ); + mDetachedHistory.tree->setSetupCellCb( []( UITableCell* cell ) { + cell->on( Event::OnTooltipCreated, []( const Event* event ) { + auto* tooltip = event->getNode()->asType()->getTooltip(); + tooltip->setMaxWidthEq( "60%" ); + tooltip->setWordWrap( true ); + tooltip->setHorizontalAlign( UI_HALIGN_LEFT ); + } ); + } ); + mDetachedHistory.tree->on( Event::OnModelEvent, [this]( const Event* event ) { + const auto* modelEvent = static_cast( event ); + if ( modelEvent->getModelEventType() == ModelEventType::OpenTree ) + activateHistoryIndex( modelEvent->getModelIndex(), true ); + else if ( modelEvent->getModelEventType() == ModelEventType::Open ) + activateHistoryIndex( modelEvent->getModelIndex(), false ); + else if ( modelEvent->getModelEventType() == ModelEventType::OpenMenu ) + openHistoryMenu( modelEvent->getModelIndex() ); + } ); + mDetachedHistory.tree->setOnSelection( [this]( const ModelIndex& index ) { + if ( const auto* node = mHistoryModel ? mHistoryModel->node( index ) : nullptr; + node && node->type == GitHistoryModel::NodeType::Commit ) + openDetachedCommitDetails( node->commit ); + } ); + auto* view = mDetachedHistory.view; + mDetachedHistory.closeConnection = + view->connect( Event::OnClose, [this, view]( const Event* ) { + if ( mDetachedHistory.view != view ) + return; + getPluginContext()->getConfig().iniState.setValue( + "git", "commit_history_columns", + mDetachedHistory.tree->serializeColumnWidths().dump() ); + mDetachedHistory.reset(); + } ); + auto* tab = mManager->getSplitter() + ->createWidget( mDetachedHistory.view, detachedHistoryTitle(), true ) + .first; + if ( tab ) + tab->setIcon( iconDrawable( "history", 12 ) ); + } else { + auto tabs = + mManager->getSplitter()->getTabFromOwnedWidgetId( mDetachedHistory.view->getId() ); + if ( !tabs.empty() ) + tabs.front().second->setTabSelected( tabs.front().first ); + } + + focusDetachedHistory(); +} + +void GitPlugin::focusDetachedHistory() { + if ( !mDetachedHistory.tree || !mHistoryModel || !mHistoryLoaded ) + return; + ModelIndex index; + if ( mDetachedHistory.focusHash.empty() ) + index = mHistoryModel->index( 0, GitHistoryModel::Subject ); + else + index = mHistoryModel->indexForCommit( mDetachedHistory.focusHash ); + if ( !index.isValid() ) + return; + mDetachedHistory.tree->setSelection( index, true, true ); + if ( const auto* node = mHistoryModel->node( index ); + node && node->type == GitHistoryModel::NodeType::Commit && + ( !mDetachedHistory.details.view || + mDetachedHistory.details.commit.hash != node->commit.hash ) ) + openDetachedCommitDetails( node->commit ); +} + +void GitPlugin::openDetachedCommitDetails( const Git::Commit& commit ) { + if ( commit.hash.empty() || !mDetachedHistory.view ) + return; + if ( !mDetachedHistory.detailsHost ) { + mDetachedHistory.detailsHost = UILinearLayout::NewVertical(); + mDetachedHistory.detailsHost->setId( "git_history_detached_details" ); + mDetachedHistory.detailsHost->setLayoutSizePolicy( SizePolicy::MatchParent, + SizePolicy::MatchParent ); + mDetachedHistory.detailsHost->setParent( mDetachedHistory.view ); + mDetachedHistory.view->setSplitPartition( StyleSheetLength( "55%" ) ); + } + mDetachedHistory.details.openCommitDetails( *this, commit, true ); +} + void GitPlugin::openCommitDetails( const Git::Commit& commit ) { + mCommitDetails.openCommitDetails( *this, commit, false ); +} + +void GitPlugin::CommitDetailsState::openCommitDetails( GitPlugin& plugin, const Git::Commit& commit, + bool detached ) { if ( commit.hash.empty() ) return; - const std::string repo = repoSelected(); - const Uint64 generation = ++mCommitDetailsGeneration; - mCommitDetailsCommit = commit; - mCommitDetailsRepo = repo; + const std::string selectedRepo = plugin.repoSelected(); + const Uint64 requestGeneration = ++generation; + this->commit = commit; + repo = selectedRepo; - const bool newView = - !mCommitDetailsView || !mManager->getSplitter()->ownedWidgetExists( mCommitDetailsView ); - if ( newView ) { - mCommitDetailsView = getUISceneNode()->loadLayoutFromString( R"xml( + const bool createView = !view; + if ( createView ) { + view = plugin.getUISceneNode()->loadLayoutFromString( R"xml( - - - - - + margin-left="8dp" visible="false" class="git_commit_btn" /> + + + + - - + + + + icon="icon(collapse-all, 12dp)" class="git_commit_btn" /> + margin-left="4dp" class="git_commit_btn" /> - )xml" ); - mCommitDetailsSubject = mCommitDetailsView->find( "git_commit_subject" ); - mCommitDetailsMetadata = mCommitDetailsView->find( "git_commit_metadata" ); - mCommitDetailsParents = mCommitDetailsView->find( "git_commit_parents" ); - mCommitDetailsMessage = mCommitDetailsView->find( "git_commit_message" ); - mCommitDetailsStatus = mCommitDetailsView->find( "git_commit_files_status" ); - mCommitDetailsMessageToggle = - mCommitDetailsView->find( "git_commit_message_toggle" ); - mCommitDetailsFilesToggle = - mCommitDetailsView->find( "git_commit_files_toggle" ); - mCommitDetailsModeToggle = - mCommitDetailsView->find( "git_commit_mode_toggle" ); - mCommitDetailsGitHub = mCommitDetailsView->find( "git_commit_github" ); - mCommitDetailsDiffContainer = mCommitDetailsView->find( "git_commit_diff" ); - mCommitDetailsMessageToggle->onClick( [this]( const Event* ) { - mCommitDetailsMessageExpanded = !mCommitDetailsMessageExpanded; - mCommitDetailsMessage->setVisible( mCommitDetailsMessageExpanded ); - mCommitDetailsMessageToggle->setText( - mCommitDetailsMessageExpanded - ? i18n( "git_collapse_commit_description", "Collapse Commit Description" ) - : i18n( "git_expand_commit_description", "Expand Commit Description" ) ); + if ( detached ) + view->setParent( plugin.mDetachedHistory.detailsHost ); + auto* state = this; + auto* owner = &plugin; + view->bind( "git_commit_subject", subject ); + view->bind( "git_commit_author", author ); + view->bind( "git_commit_date_email", dateEmail ); + view->bind( "git_commit_message", message ); + view->bind( "git_commit_files_status", status ); + view->bind( "git_commit_message_toggle", messageToggle ); + view->bind( "git_commit_files_toggle", filesToggle ); + view->bind( "git_commit_mode_toggle", modeToggle ); + view->bind( "git_commit_github", gitHub ); + view->bind( "git_commit_diff", diffContainer ); + messageToggle->onClick( [owner, state]( const Event* ) { + state->messageExpanded = !state->messageExpanded; + state->message->setVisible( state->messageExpanded ); + state->messageToggle->setText( + state->messageExpanded + ? owner->i18n( "git_collapse_commit_description", + "Collapse Commit Description" ) + : owner->i18n( "git_expand_commit_description", "Expand Commit Description" ) ); } ); - mCommitDetailsFilesToggle->onClick( [this]( const Event* ) { - mCommitDetailsFilesCollapsed = !mCommitDetailsFilesCollapsed; - UIDiffView::setMultiFileCollapsed( mCommitDetailsDiff, mCommitDetailsFilesCollapsed ); - mCommitDetailsFilesToggle->setTooltipText( - mCommitDetailsFilesCollapsed - ? i18n( "git_expand_all_files", "Expand All Files" ) - : i18n( "git_collapse_all_files", "Collapse All Files" ) ); + filesToggle->onClick( [owner, state]( const Event* ) { + state->filesCollapsed = !state->filesCollapsed; + UIDiffView::setMultiFileCollapsed( state->diff, state->filesCollapsed ); + state->filesToggle->setTooltipText( + state->filesCollapsed + ? owner->i18n( "git_expand_all_files", "Expand All Files" ) + : owner->i18n( "git_collapse_all_files", "Collapse All Files" ) ); if ( auto* icon = - findIcon( mCommitDetailsFilesCollapsed ? "expand-all" : "collapse-all" ) ) - mCommitDetailsFilesToggle->setIcon( - icon->createDrawable( PixelDensity::dpToPxI( 12 ) ) ); + owner->findIcon( state->filesCollapsed ? "expand-all" : "collapse-all" ) ) + state->filesToggle->setIcon( icon->createDrawable( PixelDensity::dpToPxI( 12 ) ) ); } ); - mCommitDetailsModeToggle->onClick( [this]( const Event* ) { - mCommitDetailsViewMode = mCommitDetailsViewMode == UIDiffView::ViewMode::Unified - ? UIDiffView::ViewMode::SideBySide - : UIDiffView::ViewMode::Unified; - UIDiffView::setMultiFileViewMode( mCommitDetailsDiff, mCommitDetailsViewMode ); - mCommitDetailsModeToggle->setText( mCommitDetailsViewMode == - UIDiffView::ViewMode::Unified - ? i18n( "git_split_diff", "Split" ) - : i18n( "git_unified_diff", "Unified" ) ); - if ( auto* icon = findIcon( mCommitDetailsViewMode == UIDiffView::ViewMode::Unified - ? "split-horizontal" - : "layout" ) ) - mCommitDetailsModeToggle->setIcon( - icon->createDrawable( PixelDensity::dpToPxI( 12 ) ) ); + modeToggle->onClick( [owner, state]( const Event* ) { + state->viewMode = state->viewMode == UIDiffView::ViewMode::Unified + ? UIDiffView::ViewMode::SideBySide + : UIDiffView::ViewMode::Unified; + UIDiffView::setMultiFileViewMode( state->diff, state->viewMode ); + state->modeToggle->setText( state->viewMode == UIDiffView::ViewMode::Unified + ? owner->i18n( "git_split_diff", "Split" ) + : owner->i18n( "git_unified_diff", "Unified" ) ); + state->modeToggle->setTooltipText( + state->viewMode == UIDiffView::ViewMode::Unified + ? owner->i18n( "git_switch_to_split_diff", "Switch to split diff view" ) + : owner->i18n( "git_switch_to_unified_diff", "Switch to unified diff view" ) ); + if ( auto* icon = owner->findIcon( state->viewMode == UIDiffView::ViewMode::Unified + ? "split-horizontal" + : "layout" ) ) + state->modeToggle->setIcon( icon->createDrawable( PixelDensity::dpToPxI( 12 ) ) ); } ); - mCommitDetailsGitHub->onClick( [this]( const Event* ) { - if ( !mCommitDetailsURL.empty() ) - Engine::instance()->openURI( mCommitDetailsURL ); + gitHub->onClick( [state]( const Event* ) { + if ( !state->url.empty() ) + Engine::instance()->openURI( state->url ); } ); - mCommitDetailsView->find( "git_commit_sha" ) - ->onClick( [this]( const Event* ) { - getUISceneNode()->getWindow()->getClipboard()->setText( mCommitDetailsCommit.hash ); - } ); - auto* view = mCommitDetailsView; - mCommitDetailsCloseConnection = - view->connect( Event::OnClose, [this, view]( const Event* ) { - if ( mCommitDetailsView != view ) - return; - ++mCommitDetailsGeneration; - mCommitDetailsView = nullptr; - mCommitDetailsSubject = nullptr; - mCommitDetailsMetadata = nullptr; - mCommitDetailsParents = nullptr; - mCommitDetailsMessage = nullptr; - mCommitDetailsStatus = nullptr; - mCommitDetailsMessageToggle = nullptr; - mCommitDetailsFilesToggle = nullptr; - mCommitDetailsModeToggle = nullptr; - mCommitDetailsGitHub = nullptr; - mCommitDetailsDiffContainer = nullptr; - mCommitDetailsDiff = nullptr; - mCommitDetailsMessageBody.clear(); - mCommitDetailsURL.clear(); - mCommitDetailsRepo.clear(); + view->find( "git_commit_show_history" ) + ->onClick( + [owner, state]( const Event* ) { owner->showGitHistory( &state->commit ); } ); + view->find( "git_commit_sha" )->onClick( [owner, state]( const Event* ) { + owner->getUISceneNode()->getWindow()->getClipboard()->setText( state->commit.hash ); + } ); + if ( !detached ) { + auto* view = this->view; + closeConnection = view->connect( Event::OnClose, [state, view]( const Event* ) { + if ( state->view == view ) + state->reset(); } ); + } } - mCommitDetailsSubject->setText( String::fromUtf8( commit.subject ) ); - String metadata = String::fromUtf8( commit.authorName ) + "\n" + - Sys::epochToString( commit.commitTime ) + " - " + - String::fromUtf8( commit.authorEmail ); - mCommitDetailsMetadata->setText( metadata ); - String parents; - for ( size_t i = 0; i < commit.parents.size(); ++i ) { - if ( i ) - parents += ", "; - parents += String::fromUtf8( commit.parents[i] ); - } - mCommitDetailsMessageBody.clear(); - mCommitDetailsMessageExpanded = false; - mCommitDetailsMessage->setText( "" ); - mCommitDetailsMessage->setVisible( false ); - mCommitDetailsMessageToggle->setVisible( false ); - mCommitDetailsParents->setVisible( !parents.empty() ); - mCommitDetailsParents->setText( i18n( "git_parents", "Parents" ) + ": " + parents ); - mCommitDetailsStatus->setText( - i18n( "git_loading_changed_files", "Loading changed files..." ) ); - mCommitDetailsFilesCollapsed = false; - mCommitDetailsFilesToggle->setTooltipText( - i18n( "git_collapse_all_files", "Collapse All Files" ) ); - if ( auto* icon = findIcon( "collapse-all" ) ) - mCommitDetailsFilesToggle->setIcon( icon->createDrawable( PixelDensity::dpToPxI( 12 ) ) ); - mCommitDetailsURL.clear(); - mCommitDetailsGitHub->setVisible( false ); - mCommitDetailsDiff = nullptr; - mCommitDetailsDiffContainer->closeAllChildren(); - auto* shaButton = mCommitDetailsView->find( "git_commit_sha" ); - shaButton->setText( String::fromUtf8( commit.shortHash ) ); - shaButton->setTooltipText( String::fromUtf8( commit.hash ) ); + subject->setText( String::fromUtf8( commit.subject ) ); + author->setText( String::fromUtf8( commit.authorName ) ); + dateEmail->setText( Sys::epochToString( commit.commitTime ) + " - " + + String::fromUtf8( commit.authorEmail ) ); + messageBody.clear(); + messageExpanded = false; + message->setText( "" ); + message->setVisible( false ); + messageToggle->setVisible( false ); + status->setText( plugin.i18n( "git_loading_changed_files", "Loading changed files..." ) ); + plugin.styleCommitFilesStatus( status ); + filesCollapsed = false; + filesToggle->setTooltipText( plugin.i18n( "git_collapse_all_files", "Collapse All Files" ) ); + if ( auto* icon = plugin.findIcon( "collapse-all" ) ) + filesToggle->setIcon( icon->createDrawable( PixelDensity::dpToPxI( 12 ) ) ); + url.clear(); + gitHub->setVisible( false ); + diff = nullptr; + diffContainer->closeAllChildren(); + auto* shaButton = view->find( "git_commit_sha" ); + shaButton->setTooltipText( String::format( + plugin.i18n( "git_copy_commit_sha", "Copy Commit SHA\n%s" ).toUtf8(), commit.hash ) ); const std::string tabName = commit.shortHash + " " + commit.subject; - if ( newView ) { - mManager->getSplitter()->createWidget( mCommitDetailsView, tabName, true ); - } else { - auto tabs = mManager->getSplitter()->getTabFromOwnedWidgetId( mCommitDetailsView->getId() ); + if ( !detached && !plugin.mManager->getSplitter()->ownedWidgetExists( view ) ) { + auto tab = plugin.mManager->getSplitter()->createWidget( view, tabName, true ).first; + if ( tab ) + tab->setIcon( plugin.iconDrawable( "git-commit", 12 ) ); + } else if ( !detached ) { + auto tabs = plugin.mManager->getSplitter()->getTabFromOwnedWidgetId( view->getId() ); if ( !tabs.empty() ) { tabs.front().first->setText( tabName ); tabs.front().second->setTabSelected( tabs.front().first ); } } - if ( generation == mCommitDetailsGeneration ) - loadCommitFiles(); + if ( requestGeneration == generation ) + loadCommitFiles( plugin, detached ); } -void GitPlugin::loadCommitFiles() { - const Uint64 generation = mCommitDetailsGeneration; - const std::string repo = mCommitDetailsRepo; - const Git::Commit commit = mCommitDetailsCommit; - auto git = mGit; - const auto lifetime = mLifetime.weakHandle(); - runAsyncTask( [git = std::move( git ), lifetime, generation, repo, commit] { +void GitPlugin::CommitDetailsState::loadCommitFiles( GitPlugin& plugin, bool detached ) { + const Uint64 generation = this->generation; + const std::string repo = this->repo; + const Git::Commit commit = this->commit; + auto git = plugin.mGit; + const auto lifetime = plugin.mLifetime.weakHandle(); + plugin.runAsyncTask( [git = std::move( git ), lifetime, generation, repo, commit, detached] { auto result = git->commitFiles( commit, repo ); - lifetime.run( [generation, repo, commit, + lifetime.run( [generation, repo, commit, detached, result = std::move( result )]( GitPlugin* plugin ) mutable { - if ( plugin->mShuttingDown || generation != plugin->mCommitDetailsGeneration || - repo != plugin->mCommitDetailsRepo || repo != plugin->repoSelected() || - commit.hash != plugin->mCommitDetailsCommit.hash || !plugin->mCommitDetailsView || - !plugin->mManager->getSplitter()->ownedWidgetExists( plugin->mCommitDetailsView ) ) + auto& details = detached ? plugin->mDetachedHistory.details : plugin->mCommitDetails; + if ( plugin->mShuttingDown || generation != details.generation || + repo != details.repo || repo != plugin->repoSelected() || + commit.hash != details.commit.hash || !details.view || + ( detached + ? !plugin->mDetachedHistory.view + : !plugin->mManager->getSplitter()->ownedWidgetExists( details.view ) ) ) return; if ( result.fail() ) { - plugin->mCommitDetailsStatus->setText( + details.status->setText( plugin->i18n( "git_changed_files_error", "Could not load changed files" ) + ( result.result.empty() ? "" : ": " + result.result ) ); + plugin->styleCommitFilesStatus( details.status ); return; } std::string message = std::move( result.message ); const size_t subjectEnd = message.find_first_of( "\r\n" ); const std::string_view subject{ message.data(), subjectEnd == std::string::npos ? message.size() : subjectEnd }; - plugin->mCommitDetailsSubject->setText( + details.subject->setText( String::fromUtf8( subject.empty() ? commit.subject : std::string{ subject } ) ); std::string body; if ( subjectEnd != std::string::npos ) { @@ -2706,14 +2896,12 @@ void GitPlugin::loadCommitFiles() { ++bodyStart; body = message.substr( bodyStart ); } - plugin->mCommitDetailsMessageBody = std::move( body ); - plugin->mCommitDetailsMessage->setText( - String::fromUtf8( plugin->mCommitDetailsMessageBody ) ); - plugin->mCommitDetailsMessage->setVisible( false ); - plugin->mCommitDetailsMessageExpanded = false; - plugin->mCommitDetailsMessageToggle->setVisible( - !plugin->mCommitDetailsMessageBody.empty() ); - plugin->mCommitDetailsMessageToggle->setText( + details.messageBody = std::move( body ); + details.message->setText( String::fromUtf8( details.messageBody ) ); + details.message->setVisible( false ); + details.messageExpanded = false; + details.messageToggle->setVisible( !details.messageBody.empty() ); + details.messageToggle->setText( plugin->i18n( "git_expand_commit_description", "Expand Commit Description" ) ); int totalInserts = 0; @@ -2723,33 +2911,34 @@ void GitPlugin::loadCommitFiles() { totalDeletes += file.deletes; } if ( result.files.empty() ) { - plugin->mCommitDetailsStatus->setText( + details.status->setText( plugin->i18n( "git_no_changed_files", "No changed files" ) ); } else { - plugin->mCommitDetailsStatus->setText( String::format( + details.status->setText( String::format( plugin->i18n( "git_changed_files_summary", "Changed files (%zu) +%d -%d" ) .toUtf8(), result.files.size(), totalInserts, totalDeletes ) ); } + plugin->styleCommitFilesStatus( details.status ); - plugin->mCommitDetailsURL = std::move( result.commitURL ); - plugin->mCommitDetailsGitHub->setVisible( !plugin->mCommitDetailsURL.empty() ); - plugin->mCommitDetailsDiffContainer->closeAllChildren(); - plugin->mCommitDetailsDiff = nullptr; + details.url = std::move( result.commitURL ); + details.gitHub->setVisible( !details.url.empty() ); + details.diffContainer->closeAllChildren(); + details.diff = nullptr; if ( !result.patch.empty() ) { - plugin->mCommitDetailsDiff = UIDiffView::NewMultiFileDiffViewer( - result.patch, repo, plugin->mCommitDetailsViewMode ); - plugin->mCommitDetailsDiff->setLayoutSizePolicy( SizePolicy::MatchParent, - SizePolicy::MatchParent ); - plugin->mCommitDetailsDiff->setParent( plugin->mCommitDetailsDiffContainer ); - for ( auto* diff : UIDiffView::multiFileDiffViews( plugin->mCommitDetailsDiff ) ) { + details.diff = + UIDiffView::NewMultiFileDiffViewer( result.patch, repo, details.viewMode ); + details.diff->setLayoutSizePolicy( SizePolicy::MatchParent, + SizePolicy::MatchParent ); + details.diff->setParent( details.diffContainer ); + for ( auto* diff : UIDiffView::multiFileDiffViews( details.diff ) ) { if ( const auto* scheme = plugin->getPluginContext()->getCurrentColorScheme() ) diff->setSyntaxColorScheme( *scheme ); } } - const bool hasDiff = plugin->mCommitDetailsDiff != nullptr; - plugin->mCommitDetailsFilesToggle->setVisible( hasDiff ); - plugin->mCommitDetailsModeToggle->setVisible( hasDiff ); + const bool hasDiff = details.diff != nullptr; + details.filesToggle->setVisible( hasDiff ); + details.modeToggle->setVisible( hasDiff ); } ); } ); } @@ -2942,6 +3131,34 @@ void GitPlugin::buildSidePanelTab() { treeview::cell.git_highlight_style_clear > treeview::cell::icon { foreground-image: none, none; } + #git_commit_details .git_commit_btn { + lw: 20dp; + lh: 20dp; + padding: 0; + background-color: var(--list-back); + border-color: transparent; + } + #git_commit_details .git_commit_btn:hover { + border-color: var(--primary); + } + #git_commit_details #git_commit_author { + font-size: 11dp; + text-stroke-width: 1dp; + text-stroke-color: var(--list-back); + layout-gravity: center_vertical; + } + #git_commit_details #git_commit_date_email { + color: var(--font-hint); + } + #git_commit_details #git_commit_subject { + font-size: 13dp; + text-overflow: ellipsis; + word-wrap: true; + } + #git_commit_details #git_commit_sha { + lw: wc; + padding: 0dp 4dp; + } @@ -3022,6 +3239,7 @@ void GitPlugin::buildSidePanelTab() { if ( revision == mHistoryRevision ) return; mHistoryRevision = revision; + updateDetachedHistoryTitle(); invalidateHistory(); } ); updateHistoryRefs( @@ -3124,8 +3342,13 @@ void GitPlugin::buildSidePanelTab() { const auto* modelEvent = static_cast( event ); if ( modelEvent->getModelEventType() == ModelEventType::OpenTree ) activateHistoryIndex( modelEvent->getModelIndex(), true ); - else if ( modelEvent->getModelEventType() == ModelEventType::Open ) + else if ( modelEvent->getModelEventType() == ModelEventType::Open ) { activateHistoryIndex( modelEvent->getModelIndex(), false ); + if ( const auto* node = mHistoryModel->node( modelEvent->getModelIndex() ); + node && node->type == GitHistoryModel::NodeType::Commit ) + openCommitDetails( node->commit ); + } else if ( modelEvent->getModelEventType() == ModelEventType::OpenMenu ) + openHistoryMenu( modelEvent->getModelIndex() ); } ); mHistoryTree->setOnSelection( [this]( const ModelIndex& index ) { if ( !mHistoryModel ) diff --git a/src/tools/ecode/plugins/git/gitplugin.hpp b/src/tools/ecode/plugins/git/gitplugin.hpp index 2511fc538..45814d724 100644 --- a/src/tools/ecode/plugins/git/gitplugin.hpp +++ b/src/tools/ecode/plugins/git/gitplugin.hpp @@ -25,6 +25,7 @@ class UIStackWidget; class UIListBoxItem; class UIMenu; class UITextView; +class UISplitter; } // namespace EE::UI namespace ecode { @@ -167,27 +168,74 @@ class GitPlugin : public PluginBase { std::string mHistoryRevision{ "HEAD" }; bool mUpdatingHistoryRefs{ false }; bool mHistoryLoaded{ false }; - UIWidget* mCommitDetailsView{ nullptr }; - UITextView* mCommitDetailsSubject{ nullptr }; - UITextView* mCommitDetailsMetadata{ nullptr }; - UITextView* mCommitDetailsMessage{ nullptr }; - UITextView* mCommitDetailsParents{ nullptr }; - UITextView* mCommitDetailsStatus{ nullptr }; - UIPushButton* mCommitDetailsMessageToggle{ nullptr }; - UIPushButton* mCommitDetailsFilesToggle{ nullptr }; - UIPushButton* mCommitDetailsModeToggle{ nullptr }; - UIPushButton* mCommitDetailsGitHub{ nullptr }; - UIWidget* mCommitDetailsDiffContainer{ nullptr }; - UIScrollView* mCommitDetailsDiff{ nullptr }; - std::string mCommitDetailsMessageBody; - std::string mCommitDetailsURL; - bool mCommitDetailsMessageExpanded{ false }; - bool mCommitDetailsFilesCollapsed{ false }; - Tools::UIDiffView::ViewMode mCommitDetailsViewMode{ Tools::UIDiffView::ViewMode::Unified }; - Git::Commit mCommitDetailsCommit; - std::string mCommitDetailsRepo; - std::atomic mCommitDetailsGeneration{ 0 }; - EventConnection mCommitDetailsCloseConnection; + struct CommitDetailsState { + UIWidget* view{ nullptr }; + UITextView* subject{ nullptr }; + UITextView* author{ nullptr }; + UITextView* dateEmail{ nullptr }; + UITextView* message{ nullptr }; + UITextView* status{ nullptr }; + UIPushButton* messageToggle{ nullptr }; + UIPushButton* filesToggle{ nullptr }; + UIPushButton* modeToggle{ nullptr }; + UIPushButton* gitHub{ nullptr }; + UIWidget* diffContainer{ nullptr }; + UIScrollView* diff{ nullptr }; + std::string messageBody; + std::string url; + Git::Commit commit; + std::string repo; + std::atomic generation{ 0 }; + EventConnection closeConnection; + Tools::UIDiffView::ViewMode viewMode{ Tools::UIDiffView::ViewMode::Unified }; + bool messageExpanded{ false }; + bool filesCollapsed{ false }; + + void openCommitDetails( GitPlugin& plugin, const Git::Commit& commit, bool detached ); + + void loadCommitFiles( GitPlugin& plugin, bool detached ); + + void reset() { + ++generation; + view = nullptr; + subject = nullptr; + author = nullptr; + dateEmail = nullptr; + message = nullptr; + status = nullptr; + messageToggle = nullptr; + filesToggle = nullptr; + modeToggle = nullptr; + gitHub = nullptr; + diffContainer = nullptr; + diff = nullptr; + messageBody.clear(); + url.clear(); + commit = {}; + repo.clear(); + viewMode = Tools::UIDiffView::ViewMode::Unified; + messageExpanded = false; + filesCollapsed = false; + } + }; + struct DetachedHistoryState { + UISplitter* view{ nullptr }; + UITreeView* tree{ nullptr }; + UIWidget* detailsHost{ nullptr }; + EventConnection closeConnection; + std::string focusHash; + CommitDetailsState details; + + void reset() { + view = nullptr; + tree = nullptr; + detailsHost = nullptr; + focusHash.clear(); + details.reset(); + } + }; + CommitDetailsState mCommitDetails; + DetachedHistoryState mDetachedHistory; std::atomic mRunningUpdateStatus{ 0 }; std::atomic mPendingForcedStatusUpdate{ false }; std::shared_ptr> mRunningAsyncTasks{ std::make_shared>( 0 ) }; @@ -217,6 +265,7 @@ class GitPlugin : public PluginBase { SyntaxColorScheme scheme; }; std::optional mStatusCustomTokenizer; + std::optional mCommitStatusCustomTokenizer; std::optional mTooltipCustomSyntaxDef; Uint32 mModelChangedId{ 0 }; Uint32 mModelStylerId{ 0 }; @@ -302,6 +351,8 @@ class GitPlugin : public PluginBase { void updateStatusBarSync(); + void styleCommitFilesStatus( UITextView* status ); + void updateUI(); void updateUINow( bool force = false ); @@ -324,7 +375,17 @@ class GitPlugin : public PluginBase { void openCommitDetails( const Git::Commit& commit ); - void loadCommitFiles(); + void showGitHistory( const Git::Commit* commit = nullptr ); + + void focusDetachedHistory(); + + void openHistoryMenu( const ModelIndex& index ); + + void openDetachedCommitDetails( const Git::Commit& commit ); + + std::string detachedHistoryTitle(); + + void updateDetachedHistoryTitle(); void buildSidePanelTab();