Improve Git History navigation and commit details

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.
This commit is contained in:
Martín Lucas Golini
2026-08-29 02:15:26 -03:00
parent ef962fc61f
commit 61d9d3c7bc
15 changed files with 551 additions and 204 deletions

View File

@@ -1264,6 +1264,12 @@ Für sichtbare Änderung ecode neu starten.</string>
<string name="git_history_load_older">Ältere Commits laden</string>
<string name="git_history_empty">Noch keine Commits</string>
<string name="git_history_no_additional_commits">Keine weiteren Commits</string>
<string name="git_show_in_history">Im Git-Verlauf anzeigen</string>
<string name="git_commit_history_title">Git-Commit-Verlauf %s</string>
<string name="git_commit_sha">Commit-SHA</string>
<string name="git_copy_commit_sha">Commit-SHA kopieren&#10;%s</string>
<string name="git_switch_to_split_diff">Zur geteilten Diff-Ansicht wechseln</string>
<string name="git_switch_to_unified_diff">Zur vereinheitlichten Diff-Ansicht wechseln</string>
<string name="git_history_error">Git-Verlauf konnte nicht geladen werden</string>
<string name="git_history_retry">Wiederholen</string>
<string name="git_local_branch">Lokal</string>

View File

@@ -1249,6 +1249,12 @@ Restart ecode to see the changes.</string>
<string name="git_history_load_older">Load older commits</string>
<string name="git_history_empty">No commits yet</string>
<string name="git_history_no_additional_commits">No additional commits</string>
<string name="git_show_in_history">Show in Git History</string>
<string name="git_commit_history_title">Git Commit History - %s</string>
<string name="git_commit_sha">Commit SHA</string>
<string name="git_copy_commit_sha">Copy Commit SHA&#10;%s</string>
<string name="git_switch_to_split_diff">Switch to split diff view</string>
<string name="git_switch_to_unified_diff">Switch to unified diff view</string>
<string name="git_history_error">Could not load Git history</string>
<string name="git_history_retry">Retry</string>
<string name="git_local_branch">Local</string>

View File

@@ -1248,6 +1248,12 @@ Redémarrer ecode pour voir les changements.</string>
<string name="git_history_load_older">Charger les commits précédents</string>
<string name="git_history_empty">Aucun commit pour le moment</string>
<string name="git_history_no_additional_commits">Aucun commit supplémentaire</string>
<string name="git_show_in_history">Afficher dans lhistorique Git</string>
<string name="git_commit_history_title">Historique des commits Git %s</string>
<string name="git_commit_sha">SHA du commit</string>
<string name="git_copy_commit_sha">Copier le SHA du commit&#10;%s</string>
<string name="git_switch_to_split_diff">Passer à la vue des différences séparée</string>
<string name="git_switch_to_unified_diff">Passer à la vue des différences unifiée</string>
<string name="git_history_error">Impossible de charger lhistorique Git</string>
<string name="git_history_retry">Réessayer</string>
<string name="git_local_branch">Locale</string>

View File

@@ -1053,6 +1053,12 @@ file in the directory tree.</string>
<string name="git_history_load_older">加载更早的提交</string>
<string name="git_history_empty">尚无提交</string>
<string name="git_history_no_additional_commits">没有其他提交</string>
<string name="git_show_in_history">在 Git 历史记录中显示</string>
<string name="git_commit_history_title">Git 提交历史记录 - %s</string>
<string name="git_commit_sha">提交 SHA</string>
<string name="git_copy_commit_sha">复制提交 SHA&#10;%s</string>
<string name="git_switch_to_split_diff">切换到拆分差异视图</string>
<string name="git_switch_to_unified_diff">切换到统一差异视图</string>
<string name="git_history_error">无法加载 Git 历史记录</string>
<string name="git_history_retry">重试</string>
<string name="git_local_branch">本地</string>

View File

@@ -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<CSS::StyleSheetProperty> mPropertyResolutionSlot;
std::vector<std::unique_ptr<CSS::StyleSheetProperty>> mNestedPropertyResolutionSlots;
SmallVector<std::unique_ptr<CSS::StyleSheetProperty>, 2> mNestedPropertyResolutionSlots;
bool mChangingState;
bool mForceReapplyProperties;
bool mDisableAnimations;

View File

@@ -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.
*

View File

@@ -261,6 +261,8 @@ ResourcePtr<UIIconTheme> 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 },

View File

@@ -735,6 +735,11 @@ void UITextView::onAlignChange() {
}
void UITextView::onSelectionChange() {
if ( mUsingCustomStyling ) {
invalidateDraw();
return;
}
mTextCache.invalidateColors();
if ( selCurInit() != selCurEnd() ) {

View File

@@ -308,7 +308,7 @@ UIWidget* UITreeView::updateCell( const Vector2<Int64>& 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 );

View File

@@ -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<Uint32>( pseudoCls ) ) != 0;
}
bool UIWidget::isTooltipEnabled() const {
return ( mFlags & UI_TOOLTIP_ENABLED ) != 0;
}

View File

@@ -2,6 +2,7 @@
#include "gitplugin.hpp"
#include <algorithm>
#include <eepp/system/sys.hpp>
#include <functional>
#include <iterator>
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<const Node*( const Nodes& )> 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 );

View File

@@ -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 );

View File

@@ -50,8 +50,8 @@ void GitHistoryTreeViewCell::draw() {
if ( mMetadataText.getString().empty() )
return;
const bool selected = getParent() && ( getParent()->asType<UIWidget>()->getStyleState() &
UIState::StateFlagSelected );
const bool selected = getParent() && ( getParent()->asType<UIWidget>()->hasPseudoClass(
StyleSheetSelectorRule::PseudoClasses::Selected ) );
mMetadataText.setFillColor( selected ? mTextBox->getFontColor() : mHintColor );
mMetadataText.draw( std::floor( mScreenPos.x + mTextBox->getPixelsPosition().x ),

View File

@@ -1,4 +1,5 @@
#include "gitplugin.hpp"
#include "../../appconfig.hpp"
#include "../../settingspage.hpp"
#include "gitbranchmodel.hpp"
#include "githistorymodel.hpp"
@@ -22,6 +23,7 @@
#include <eepp/ui/uipopupmenu.hpp>
#include <eepp/ui/uiradiobutton.hpp>
#include <eepp/ui/uiscrollview.hpp>
#include <eepp/ui/uisplitter.hpp>
#include <eepp/ui/uistackwidget.hpp>
#include <eepp/ui/uistyle.hpp>
#include <eepp/ui/uitextedit.hpp>
@@ -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<SyntaxPattern> 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<GitBranchModel>& 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<UIMenuItem>()->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(
<Splitter id="git_history_detached" lw="mp" lh="mp" orientation="horizontal"
splitter-partition="55%">
<vbox lw="0" lw8="1" lh="mp">
<TreeView id="git_history_detached_tree" lw="mp" lh="mp" />
</vbox>
</Splitter>
)xml" )
->asType<UISplitter>();
mDetachedHistory.view->setAlwaysShowSplitter( false );
mDetachedHistory.view->setHideSplitterOnEdge( true );
mDetachedHistory.view->setSplitPartition( StyleSheetLength( "100%" ) );
mDetachedHistory.tree =
mDetachedHistory.view->find<UITreeView>( "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<UIWidget>()->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<const ModelEvent*>( 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(
<vbox id="git_commit_details" lw="mp" lh="mp">
<vbox lw="mp" lh="wc" padding="12dp">
<hbox lw="mp" lh="wc">
<TextView id="git_commit_metadata" lw="0" lw8="1" lh="wc"
word-wrap="true" focusable="false" />
<PushButton id="git_commit_sha" text="Commit SHA"
text-as-fallback="true" margin-left="8dp" />
</hbox>
<hbox lw="mp" lh="wc" margin-top="8dp">
<TextView id="git_commit_subject" lw="0" lw8="1" lh="wc"
font-size="14dp" word-wrap="false" text-overflow="ellipsis"
<TextView id="git_commit_author" lw="wc" lh="wc" word-wrap="true"
focusable="false" />
<PushButton id="git_commit_message_toggle"
text="@string(git_expand_commit_description, Expand Commit Description)"
icon="icon(unfold, 12dp)" text-as-fallback="true"
margin-left="8dp" visible="false" />
margin-left="8dp" visible="false" class="git_commit_btn" />
<widget lw="0" lw8="1" lh="wc" />
<PushButton id="git_commit_show_history"
tooltip="@string(git_show_in_history, Show in Git History)"
icon="icon(history, 12dp)" margin-left="8dp" class="git_commit_btn" />
<PushButton id="git_commit_github"
text="@string(git_view_on_github, View on GitHub)"
tooltip="@string(git_view_on_github, View on GitHub)"
icon="icon(github, 12dp)" text-as-fallback="true"
margin-left="8dp" visible="false" class="git_commit_btn" />
<PushButton id="git_commit_sha"
text="@string(git_commit_sha, Commit SHA)" icon="icon(copy, 12dp)"
margin-left="8dp" class="git_commit_btn" />
</hbox>
<TextView id="git_commit_message" lw="mp" lh="wc" margin-top="8dp"
word-wrap="true" focusable="false" visible="false" />
<TextView id="git_commit_parents" lw="mp" lh="wc" margin-top="4dp"
word-wrap="true" focusable="false" />
<TextView id="git_commit_date_email" lw="mp" lh="wc" word-wrap="true"
focusable="false" />
<TextView id="git_commit_subject" lw="mp" lh="wc" margin-top="8dp"
focusable="false" />
<TextView id="git_commit_message" lw="mp" lh="wc" word-wrap="true"
focusable="false" visible="false" />
</vbox>
<hbox lw="mp" lh="wc" padding-left="8dp" padding-right="8dp"
padding-top="4dp" padding-bottom="4dp">
<PushButton id="git_commit_files_toggle"
tooltip="@string(git_collapse_all_files, Collapse All Files)"
icon="icon(collapse-all, 12dp)" />
icon="icon(collapse-all, 12dp)" class="git_commit_btn" />
<PushButton id="git_commit_mode_toggle"
text="@string(git_split_diff, Split)"
tooltip="@string(git_switch_to_split_diff, Switch to split diff view)"
icon="icon(split-horizontal, 12dp)" text-as-fallback="true"
margin-left="4dp" />
margin-left="4dp" class="git_commit_btn" />
<TextView id="git_commit_files_status" lw="0" lw8="1" lh="wc"
margin-left="8dp" layout_gravity="center_vertical" focusable="false" />
<PushButton id="git_commit_github"
text="@string(git_view_on_github, View on GitHub)"
icon="icon(github, 12dp)" text-as-fallback="true"
visible="false" />
</hbox>
<vbox id="git_commit_diff" lw="mp" lh="0" lw8="1" />
</vbox>
)xml" );
mCommitDetailsSubject = mCommitDetailsView->find<UITextView>( "git_commit_subject" );
mCommitDetailsMetadata = mCommitDetailsView->find<UITextView>( "git_commit_metadata" );
mCommitDetailsParents = mCommitDetailsView->find<UITextView>( "git_commit_parents" );
mCommitDetailsMessage = mCommitDetailsView->find<UITextView>( "git_commit_message" );
mCommitDetailsStatus = mCommitDetailsView->find<UITextView>( "git_commit_files_status" );
mCommitDetailsMessageToggle =
mCommitDetailsView->find<UIPushButton>( "git_commit_message_toggle" );
mCommitDetailsFilesToggle =
mCommitDetailsView->find<UIPushButton>( "git_commit_files_toggle" );
mCommitDetailsModeToggle =
mCommitDetailsView->find<UIPushButton>( "git_commit_mode_toggle" );
mCommitDetailsGitHub = mCommitDetailsView->find<UIPushButton>( "git_commit_github" );
mCommitDetailsDiffContainer = mCommitDetailsView->find<UIWidget>( "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<UIPushButton>( "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<UIPushButton>( "git_commit_show_history" )
->onClick(
[owner, state]( const Event* ) { owner->showGitHistory( &state->commit ); } );
view->find<UIPushButton>( "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<UIPushButton>( "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<UIPushButton>( "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;
}
</style>
<RelativeLayout id="git_panel" lw="mp" lh="mp">
<vbox id="git_content" lw="mp" lh="mp">
@@ -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<const ModelEvent*>( 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 )

View File

@@ -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<Uint64> 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<Uint64> 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<int> mRunningUpdateStatus{ 0 };
std::atomic<bool> mPendingForcedStatusUpdate{ false };
std::shared_ptr<std::atomic<int>> mRunningAsyncTasks{ std::make_shared<std::atomic<int>>( 0 ) };
@@ -217,6 +265,7 @@ class GitPlugin : public PluginBase {
SyntaxColorScheme scheme;
};
std::optional<CustomTokenizer> mStatusCustomTokenizer;
std::optional<CustomTokenizer> mCommitStatusCustomTokenizer;
std::optional<SyntaxDefinition> 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();