// Copyright Epic Games, Inc. All Rights Reserved. #include "CoreMinimal.h" #include "Misc/MessageDialog.h" #include "Misc/Paths.h" #include "ISourceControlOperation.h" #include "SourceControlOperations.h" #include "ISourceControlRevision.h" #include "SourceControlWindows.h" #include "SourceControlHelpers.h" #include "ISourceControlModule.h" #include "SSourceControlCommon.h" #include "Modules/ModuleManager.h" #include "UObject/Object.h" #include "UObject/Package.h" #include "Misc/PackageName.h" #include "InputCoreTypes.h" #include "Layout/Visibility.h" #include "Layout/Geometry.h" #include "Widgets/SNullWidget.h" #include "Styling/SlateBrush.h" #include "Input/Events.h" #include "Input/DragAndDrop.h" #include "Input/Reply.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SWidget.h" #include "Widgets/SCompoundWidget.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SWindow.h" #include "SlateOptMacros.h" #include "Framework/Application/SlateApplication.h" #include "Textures/SlateIcon.h" #include "Framework/Commands/UIAction.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Images/SImage.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Layout/SBox.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Widgets/Input/SEditableTextBox.h" #include "Widgets/Input/SMultiLineEditableTextBox.h" #include "Widgets/Layout/SSplitter.h" #include "Widgets/Views/SExpanderArrow.h" #include "Widgets/Views/SHeaderRow.h" #include "Widgets/Views/STableViewBase.h" #include "Widgets/Views/STableRow.h" #include "Widgets/Views/STreeView.h" #include "Framework/Docking/TabManager.h" #include "Styling/AppStyle.h" #include "IAssetTools.h" #include "IAssetTypeActions.h" #include "AssetToolsModule.h" #include "DiffUtils.h" #include "ToolMenu.h" #include "ToolMenus.h" /** * Wrapper around data from ISourceControlRevision */ class FHistoryRevisionListViewItem { public: /** Changelist description */ FString Description; /** User name of submitter */ FString UserName; /** Clientspec/workspace of submitter */ FString ClientSpec; /** File action for this revision (branch, delete, edit, etc.) */ FString Action; /** Source path of branch, if any */ FString BranchSource; /** Date of this revision */ FDateTime Date; /** Number of this revision */ FString Revision; /** Changelist number */ int32 ChangelistNumber; /** Filesize for this revision (0 in the event of a deletion) */ int32 FileSize; /** * Constructor * * @param InRevision File revision info. to populate this wrapper with */ FHistoryRevisionListViewItem( const TSharedRef& InRevision ) { Description = InRevision->GetDescription(); UserName = InRevision->GetUserName(); ClientSpec = InRevision->GetClientSpec(); Action = InRevision->GetAction(); BranchSource = InRevision->GetBranchSource().IsValid() ? InRevision->GetBranchSource()->GetFilename() : TEXT(""); Date = InRevision->GetDate(); Revision = InRevision->GetRevision(); ChangelistNumber = InRevision->GetCheckInIdentifier(); FileSize = InRevision->GetFileSize(); } }; /** * Managed mirror of FSourceControlFileHistoryInfo. Designed to represent the history of a file in * a listview. */ class FHistoryFileListViewItem { public: /** Depot name of the file */ FString FileName; /** * Constructor * * @param InFileName File name of the list item */ FHistoryFileListViewItem( const FString& InFileName ) : FileName(InFileName) { } }; /** * A container class to use the tree view to represent a dynamically expandable nested list */ struct FHistoryTreeItem { // Only one of FileListItem or RevisionListItem should be set /** Pointer to file info */ TSharedPtr FileListItem; /** Pointer to revision info */ TSharedPtr RevisionListItem; /** If we are a revision entry, pointer to file entry that owns us */ TWeakPtr Parent; /** List of revisions if we are file entry */ TArray< TSharedPtr > Children; }; /** * Attempts to get a file list-item that represents the file that specified * history-tree entry belongs to. * * @param HistoryTreeItemIn The history-tree entry that you want a file item for. * @return The file list-item that the specified entry conceptually belongs to (invalid if HistoryTreeItemIn was invalid). */ static TSharedPtr GetFileListItem(TSharedPtr HistoryTreeItemIn) { TSharedPtr FileListItem; if (HistoryTreeItemIn.IsValid()) { FileListItem = HistoryTreeItemIn->FileListItem; // if this isn't a file list-item itself if (!FileListItem.IsValid()) { // then it should have a parent that is one TSharedPtr ParentFileItem = HistoryTreeItemIn->Parent.Pin(); check(ParentFileItem.IsValid()); check(ParentFileItem->FileListItem.IsValid()); FileListItem = ParentFileItem->FileListItem; } } return FileListItem; } /** * Takes a history-tree entry and attempts to find a corresponding asset object * for the specified revision. If the specified history item doesn't have a valid * RevisionListItem (it's a file list-item), we take that to represent the current * working version of the asset. * * @param HistoryTreeItemIn The history-tree entry that you want a corresponding object for. * @return A UObject that represents the asset at the specified revision (NULL if we failed to find/create one). */ static UObject* GetAssetRevisionObject(TSharedPtr HistoryTreeItemIn) { UObject* AssetObject = NULL; if (HistoryTreeItemIn.IsValid()) { UPackage* AssetPackage = NULL; // need a package to find the asset in TSharedPtr FileListItem = GetFileListItem(HistoryTreeItemIn); check(FileListItem.IsValid()); TSharedPtr RevisionListItem = HistoryTreeItemIn->RevisionListItem; // if this item is referencing a specific revision (and not the current working version of the asset) if (RevisionListItem.IsValid()) // else, { // grab details on this file's state in source control (history, etc.) ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); FSourceControlStatePtr FileSourceControlState = SourceControlProvider.GetState(FileListItem->FileName, EStateCacheUsage::Use); if (FileSourceControlState.IsValid()) { // lookup the specific revision we want TSharedPtr FileRevision = FileSourceControlState->FindHistoryRevision(RevisionListItem->Revision); FString TempPackageName; if (FileRevision.IsValid()) { // try and load the temporary package AssetPackage = DiffUtils::LoadPackageForDiff(FileRevision); } } // if FileSourceControlState.IsValid() } else // if we want the current working version of this asset { FString AssetPackageName; if (FPackageName::TryConvertFilenameToLongPackageName(FileListItem->FileName, /*out*/ AssetPackageName)) { AssetPackage = FindObject(NULL, *AssetPackageName); } } // grab the asset from the package - we assume asset name matches file name FString AssetName = FPaths::GetBaseFilename(FileListItem->FileName); AssetObject = FindObject(AssetPackage, *AssetName); if (AssetPackage && !AssetObject) { AssetObject = AssetPackage->FindAssetInPackage(); } } // if HistoryTreeItemIn.IsValid() return AssetObject; } /** * Constructs revision info for the specified history-tree entry. * * @param HistoryTreeItemIn The history-tree entry that you want revision info for. * @param RevisionInfoOut The resulting revision info (out). */ static void GetRevisionInfo(TSharedPtr HistoryTreeItemIn, FRevisionInfo& RevisionInfoOut) { RevisionInfoOut.Revision = TEXT(""); // clear the revision info (empty string is used signify the current working version) // if this is a specific revision item if (HistoryTreeItemIn.IsValid() && HistoryTreeItemIn->RevisionListItem.IsValid()) { TSharedPtr RevisionListItem = HistoryTreeItemIn->RevisionListItem; // fill out the revision info RevisionInfoOut.Revision = RevisionListItem->Revision; RevisionInfoOut.Changelist = RevisionListItem->ChangelistNumber; RevisionInfoOut.Date = RevisionListItem->Date; } } /** * Takes a array of FHistoryTreeItems and determines if the entries can all be diffed against each other. * * @param SelectedItems A array of FHistoryTreeItems that you want to diff against each other. * @param ErrorTextOut Text explaining why the selected items cannot be diffed (only valid if the return value was false). * @return True if the selected items could be diffed against each other, false if not. */ static bool CanDiffSelectedItems(TArray< TSharedPtr > const& SelectedItems, FText& ErrorTextOut) { bool bCanDiffSelected = false; if (SelectedItems.Num() > 2) { ErrorTextOut = NSLOCTEXT("SourceControlHistory", "TooManyToDiff", "Cannot diff more than two revisions."); } else if (SelectedItems.Num() < 2) { ErrorTextOut = NSLOCTEXT("SourceControlHistory", "NotEnoughToDiff", "Need to select two revisions in order to compare one against the other."); } else { TSharedPtr FirstSelection = SelectedItems[0]; TSharedPtr SecondSelection = SelectedItems[1]; if (!FirstSelection.IsValid() || !SecondSelection.IsValid()) { ErrorTextOut = NSLOCTEXT("SourceControlHistory", "InvalidSelection", "Invalid revisions selected."); } else if (FirstSelection == SecondSelection) { ErrorTextOut = NSLOCTEXT("SourceControlHistory", "CannotDiffWithSelf", "You cannot diff a revision against itself."); } else if (!FirstSelection->Parent.IsValid() && FirstSelection->RevisionListItem.IsValid() && FirstSelection->Children[0] == SecondSelection) { ErrorTextOut = NSLOCTEXT("SourceControlHistory", "CannotDiffWithSelf", "You cannot diff a revision against itself."); } else if (!SecondSelection->Parent.IsValid() && FirstSelection->RevisionListItem.IsValid() && SecondSelection->Children[0] == FirstSelection) { ErrorTextOut = NSLOCTEXT("SourceControlHistory", "CannotDiffWithSelf", "You cannot diff a revision against itself."); } else { // @TODO make sure the two selections match type (calling GetAssetRevisionObject() to compare class types is too slow) bCanDiffSelected = true; } } return bCanDiffSelected; }; /** * Takes two FHistoryTreeItems and attempts to diff them against each other (bringing up the diff window). * * @param FirstSelection The first item you want to diff. * @param SecondSelection The second item you want to diff. * @return True if a diff was performed, false if not. */ static bool DiffHistoryItems(TSharedPtr const FirstSelection, TSharedPtr const SecondSelection) { bool bDiffPerformed = false; if (FirstSelection.IsValid() && SecondSelection.IsValid()) { TSharedPtr FirstSelectionFileItem = GetFileListItem(FirstSelection); TSharedPtr SecondSelectionFileItem = GetFileListItem(SecondSelection); // we want to make sure the two selections are presented in a sensible order UObject* LeftDiffAsset = NULL; FRevisionInfo LeftVersionInfo; UObject* RightDiffAsset = NULL; FRevisionInfo RightVersionInfo; bool bIsForSingleAsset = (FirstSelectionFileItem == SecondSelectionFileItem); // if we're comparing two revisions for one asset if (bIsForSingleAsset) { bool bFirstSelectionIsCurrentVersion = FirstSelection->FileListItem.IsValid(); bool bSecondSelectionIsCurrentVersion = SecondSelection->FileListItem.IsValid(); // the second selection is the newer revision iff the first isn't the current working version, and // it's either the current working version itself, or a newer revision bool bSecondSelectionIsNewer = !bFirstSelectionIsCurrentVersion && (bSecondSelectionIsCurrentVersion ||(SecondSelection->RevisionListItem->Date > FirstSelection->RevisionListItem->Date)); if (bSecondSelectionIsNewer) { RightDiffAsset = GetAssetRevisionObject(SecondSelection); GetRevisionInfo(SecondSelection, RightVersionInfo); LeftDiffAsset = GetAssetRevisionObject(FirstSelection); GetRevisionInfo(FirstSelection, LeftVersionInfo); } else { LeftDiffAsset = GetAssetRevisionObject(SecondSelection); GetRevisionInfo(SecondSelection, LeftVersionInfo); RightDiffAsset = GetAssetRevisionObject(FirstSelection); GetRevisionInfo(FirstSelection, RightVersionInfo); } } else // else, we're comparing revisions from two separate assets { // keep them in selection order (left to right) LeftDiffAsset = GetAssetRevisionObject(FirstSelection); GetRevisionInfo(FirstSelection, LeftVersionInfo); RightDiffAsset = GetAssetRevisionObject(SecondSelection); GetRevisionInfo(SecondSelection, RightVersionInfo); } // if we have an asset object for both selections if ((LeftDiffAsset != NULL) && (RightDiffAsset != NULL)) { FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(TEXT("AssetTools")); AssetToolsModule.Get().DiffAssets(LeftDiffAsset, RightDiffAsset, LeftVersionInfo, RightVersionInfo); bDiffPerformed = true; } } return bDiffPerformed; } /** * A FDragDropOperation that represents dragging a source-control history tree item around. */ class FSourceControlHistoryRowDragDropOp : public FDragDropOperation { private: /** * Stubbed private constructor (in order to force use of the static New() method). */ FSourceControlHistoryRowDragDropOp() : PendingDropAction(EDropAction::None) { } public: /** * Allocates and registers a new FSourceControlHistoryRowDragDropOp for use. * * @return The newly allocated (and registered) instance. */ static TSharedRef New() { TSharedPtr NewOperation = MakeShareable(new FSourceControlHistoryRowDragDropOp); NewOperation->Construct(); return NewOperation.ToSharedRef(); } DRAG_DROP_OPERATOR_TYPE(FSourceControlHistoryRowDragDropOp, FDragDropOperation) struct EDropAction { enum Type { None, Diff, }; }; /** An enum value detailing what operation is queued to happen (if this item is dropped) */ EDropAction::Type PendingDropAction; /** The items that this operation is conceptually dragging around */ TArray< TSharedPtr > SelectedItems; /** Text to display with the widget being dragged around */ FText HoverText; /** * Creates the visual widget that you drag around (to help visualize the drag/drop operation. * * @return A new slate widget representing the dragged item */ virtual TSharedPtr GetDefaultDecorator() const override { return SNew(SBorder) .BorderImage(FAppStyle::GetBrush("Graph.ConnectorFeedback.Border")) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .Padding(0,0,3,0) [ SNew(SImage) .Image(this, &FSourceControlHistoryRowDragDropOp::GetIcon) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(this, &FSourceControlHistoryRowDragDropOp::GetHoverText) ] ]; } /** * Easy accessor function for getting at the HoverText variable (provides a * default one if HoverText is empty). * * @return HoverText if it's not empty, otherwise: a default string describing that dropping would result in nothing. */ FText GetHoverText() const { return !HoverText.IsEmpty() ? HoverText : NSLOCTEXT("SourceControlHistory", "DropActionToolTip_InvalidDropTarget", "Cannot drop here."); } /** * Returns a icon brush corresponding to this operation's pending drop action. * * @return An 'error' icon if there is no pending action, otherwise an 'OK' icon. */ FSlateBrush const* GetIcon() const { return PendingDropAction != EDropAction::None ? FAppStyle::GetBrush(TEXT("Graph.ConnectorFeedback.OK")) : FAppStyle::GetBrush(TEXT("Graph.ConnectorFeedback.Error")); } }; /** * Class used to construct the ordered row content for revision data */ class SHistoryRevisionListRowContent : public SMultiColumnTableRow< TSharedPtr > { public: SLATE_BEGIN_ARGS( SHistoryRevisionListRowContent ) : _RevisionListItem() {} SLATE_EVENT( FOnDragDetected, OnDragDetected ) SLATE_EVENT( FOnTableRowDragEnter, OnDragEnter ) SLATE_EVENT( FOnTableRowDragLeave, OnDragLeave ) SLATE_EVENT( FOnTableRowDrop, OnDrop ) SLATE_ARGUMENT( TSharedPtr, RevisionListItem ) /** Whether we should display the expander for this item as it has children */ SLATE_ARGUMENT( bool, HasChildren ) SLATE_END_ARGS() /** * Construct the widget * * @param InArgs A declaration from which to construct the widget */ void Construct( const FArguments& InArgs, const TSharedRef& InOwnerTableView ) { RevisionListItem = InArgs._RevisionListItem; check(RevisionListItem.IsValid()); bHasChildren = InArgs._HasChildren; SMultiColumnTableRow< TSharedPtr >::Construct( FSuperRowType::FArguments() .OnDragDetected(InArgs._OnDragDetected) .OnDragEnter(InArgs._OnDragEnter) .OnDragLeave(InArgs._OnDragLeave) .OnDrop(InArgs._OnDrop), InOwnerTableView ); } virtual TSharedRef GenerateWidgetForColumn( const FName& ColumnName ) override { check(RevisionListItem.IsValid()); if ( ColumnName == TEXT("Revision") ) { FString SCCAction = RevisionListItem->Action; FName ResourceKey; if (SCCAction == FString(TEXT("add"))) { ResourceKey = TEXT("SourceControl.Add"); } else if (SCCAction == FString(TEXT("edit"))) { ResourceKey = TEXT("SourceControl.Edit"); } else if (SCCAction == FString(TEXT("delete"))) { ResourceKey = TEXT("SourceControl.Delete"); } else if (SCCAction == FString(TEXT("branch"))) { ResourceKey = TEXT("SourceControl.Branch"); } else if (SCCAction == FString(TEXT("integrate"))) { ResourceKey = TEXT("SourceControl.Integrate"); } else { ResourceKey = TEXT("SourceControl.Edit"); } // Rows in a tree need to show an SExpanderArrow (it also indents!) to give the appearance of being a tree. return SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Right) .VAlign(VAlign_Fill) [ SNew(SExpanderArrow, SharedThis(this) ) .Visibility(this, &SHistoryRevisionListRowContent::GetExpanderVisibility) ] +SHorizontalBox::Slot() .AutoWidth() .Padding(10,0,10,0) .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew(SImage) .Image(FAppStyle::GetBrush(ResourceKey)) ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(STextBlock) .Text( FText::FromString(RevisionListItem->Revision) ) ]; } else if (ColumnName == TEXT("Changelist")) { return SNew(STextBlock) .Text( FText::AsNumber( RevisionListItem->ChangelistNumber, NULL, FInternationalization::Get().GetInvariantCulture() ) ) ; } else if (ColumnName == TEXT("Date")) { return SNew(STextBlock) .Text( RevisionListItem->Date > FDateTime::MinValue() == 0 ? FText() : FText::AsDateTime( RevisionListItem->Date ) ); } else if (ColumnName == TEXT("UserName")) { return SNew(STextBlock) .Text( FText::FromString( RevisionListItem->UserName ) ); } else if (ColumnName == TEXT("Description")) { return SNew(STextBlock) .Text(SSourceControlCommon::GetSingleLineChangelistDescription(FText::FromString(RevisionListItem->Description))) .ToolTipText(FText::FromString(RevisionListItem->Description)) .OverflowPolicy(ETextOverflowPolicy::Ellipsis); } else { return SNew(STextBlock) . Text( FText::Format( NSLOCTEXT("SourceControlHistory", "UnsupportedColumn", "Unsupported Column: {0}"), FText::FromName( ColumnName ) ) ); } } EVisibility GetExpanderVisibility() const { return bHasChildren ? EVisibility::Visible : EVisibility::Collapsed; } private: TSharedPtr RevisionListItem; /** Whether we should display the expander for this item as it has children */ bool bHasChildren; }; /** Panel designed to display the revision history of a package */ class SSourceControlHistoryWidget : public SCompoundWidget { public: SLATE_BEGIN_ARGS( SSourceControlHistoryWidget ) : _ParentWindow() , _SourceControlStates() {} SLATE_ATTRIBUTE( TSharedPtr, ParentWindow ) SLATE_ATTRIBUTE( TArray< FSourceControlStateRef >, SourceControlStates) SLATE_END_ARGS() const FName SourceControlHistoryContextMenu = TEXT("RevisionControl.History.ContextMenu"); SSourceControlHistoryWidget() { } static void CreateDiffMenu(UToolMenu* InToolMenu); void Construct( const FArguments& InArgs ) { if (!UToolMenus::Get()->IsMenuRegistered(SourceControlHistoryContextMenu)) { UToolMenu* ContextMenu = UToolMenus::Get()->RegisterMenu(SourceControlHistoryContextMenu); ContextMenu->bShouldCloseWindowAfterMenuSelection = true; ContextMenu->AddDynamicSection(NAME_None, FNewToolMenuDelegate::CreateStatic(&SSourceControlHistoryWidget::CreateDiffMenu)); } AddHistoryInfo(InArgs._SourceControlStates.Get()); ParentWindow = InArgs._ParentWindow.Get(); TSharedRef HeaderRow = SNew(SHeaderRow); const bool bUsesChangelists = ISourceControlModule::Get().GetProvider().UsesChangelists(); HeaderRow->AddColumn(SHeaderRow::FColumn::FArguments().ColumnId("Revision") .DefaultLabel(NSLOCTEXT("SourceControl.HistoryPanel.Header", "Revision", "Revision")) .FillWidth(bUsesChangelists ? 100 : 200)); if(bUsesChangelists) { HeaderRow->AddColumn(SHeaderRow::FColumn::FArguments().ColumnId("Changelist") .DefaultLabel(NSLOCTEXT("SourceControl.HistoryPanel.Header", "Changelist", "ChangeList")) .FillWidth(100)); } HeaderRow->AddColumn(SHeaderRow::FColumn::FArguments().ColumnId("Date") .DefaultLabel(NSLOCTEXT("SourceControl.HistoryPanel.Header", "Date", "Date Submitted")) .FillWidth(250)); HeaderRow->AddColumn(SHeaderRow::FColumn::FArguments().ColumnId("UserName") .DefaultLabel(NSLOCTEXT("SourceControl.HistoryPanel.Header", "UserName", "Submitted By")) .FillWidth(200)); HeaderRow->AddColumn(SHeaderRow::FColumn::FArguments().ColumnId("Description") .DefaultLabel(NSLOCTEXT("SourceControl.HistoryPanel.Header", "Description", "Description")) .FillWidth(650)); ChildSlot [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .BorderBackgroundColor(FLinearColor(0.5f,0.5f,0.5f,1.f)) [ SNew(SSplitter) .Orientation(Orient_Vertical) +SSplitter::Slot() .Value(0.5f) [ SNew(SBorder) [ SNew(SBox) .WidthOverride(600) [ SAssignNew( MainHistoryListView, SHistoryFileListType) .TreeItemsSource( &HistoryCollection ) .SelectionMode(ESelectionMode::Multi) .OnSelectionChanged(this, &SSourceControlHistoryWidget::OnRevisionPropertyChanged) .OnGenerateRow( this, &SSourceControlHistoryWidget::OnGenerateRowForHistoryFileList ) .OnGetChildren( this, &SSourceControlHistoryWidget::OnGetChildrenForHistoryFileList ) .OnContextMenuOpening(this, &SSourceControlHistoryWidget::OnCreateContextMenu ) .HeaderRow ( HeaderRow ) ] ] ] +SSplitter::Slot() .Value(0.5f) [ SAssignNew(AdditionalInfoItemsControl,SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ GetAdditionalInfoItemsControlContent() ] ] ] ]; //expand the top level nodes... for (int32 i=0; iSetItemExpansion(HistoryCollection[i],true); } ParentWindow.Pin()->SetWidgetToFocusOnActivate(MainHistoryListView); } private: /** Used to intercept Escape key press, and interpret it as a close event */ virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override { // Pressing escape returns as if the user closed the window if (InKeyEvent.GetKey() == EKeys::Escape) { ParentWindow.Pin()->RequestDestroyWindow(); return FReply::Handled(); } return FReply::Unhandled(); } /** * Constructs the "Additional Info" panel that displays specific revision info */ BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION TSharedRef GetAdditionalInfoItemsControlContent() { const float Padding = 2.f; return SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .FillWidth(0.25f) [ //Text Column SNew(SVerticalBox) +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(STextBlock) .Text(NSLOCTEXT("SourceControl.HistoryPanel.Info", "Revision", "Revision:")) ] +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(STextBlock) .Text(NSLOCTEXT("SourceControl.HistoryPanel.Info", "Date", "Date Submitted:")) ] +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(STextBlock) .Text(NSLOCTEXT("SourceControl.HistoryPanel.Info", "SubmittedBy", "Submitted By:")) ] +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(STextBlock) .Text(NSLOCTEXT("SourceControl.HistoryPanel.Info", "Action", "Action:")) ] ] +SHorizontalBox::Slot() .FillWidth(0.25f) .Padding(20,0) [ //Data column SNew(SVerticalBox) +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(SEditableText) .IsReadOnly(true) .Text(this, &SSourceControlHistoryWidget::GetRevisionNumber) ] +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(SEditableText) .IsReadOnly(true) .Text(this, &SSourceControlHistoryWidget::GetDate) ] +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(SEditableText) .IsReadOnly(true) .Text(this, &SSourceControlHistoryWidget::GetUserName) ] +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(SEditableText) .IsReadOnly(true) .Text(this, &SSourceControlHistoryWidget::GetAction) ] ] +SHorizontalBox::Slot() .FillWidth(0.25f) .Padding(50,0) [ //Text Column SNew(SVerticalBox) +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(STextBlock) .Text(NSLOCTEXT("SourceControl.HistoryPanel.Info", "Changelist", "Changelist:")) .Visibility(ISourceControlModule::Get().GetProvider().UsesChangelists() ? EVisibility::SelfHitTestInvisible : EVisibility::Hidden) ] +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(STextBlock) .Text(NSLOCTEXT("SourceControl.HistoryPanel.Info", "Workspace", "Workspace:")) ] +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(STextBlock) .Text(NSLOCTEXT("SourceControl.HistoryPanel.Info", "FileSize", "File Size:")) ] +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(STextBlock) .Text(NSLOCTEXT("SourceControl.HistoryPanel.Info", "BranchedFrom", "Branched From:")) ] ] +SHorizontalBox::Slot() .FillWidth(0.25f) .Padding(20,0) [ //Data column SNew(SVerticalBox) +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(SEditableText) .IsReadOnly(true) .Text(this, &SSourceControlHistoryWidget::GetChangelistNumber) ] +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(SEditableText) .IsReadOnly(true) .Text(this, &SSourceControlHistoryWidget::GetClientSpec) ] +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(SEditableText) .IsReadOnly(true) .Text(this, &SSourceControlHistoryWidget::GetFileSize) ] +SVerticalBox::Slot() .FillHeight(0.25f) .Padding(Padding) [ SNew(SEditableText) .IsReadOnly(true) .Text(this, &SSourceControlHistoryWidget::GetBranchedFrom) ] ] ] +SVerticalBox::Slot() .AutoHeight() .Padding(Padding, 10, Padding, 5) [ SNew(STextBlock) .Text(NSLOCTEXT("SourceControl.HistoryPanel.Info", "Description", "Description:")) ] +SVerticalBox::Slot() .FillHeight(1.0f) [ SNew(SBorder) [ SNew(SVerticalBox) +SVerticalBox::Slot() .Padding(5) [ SNew(SMultiLineEditableTextBox) .IsReadOnly(true) .AutoWrapText(true) .Text(this, &SSourceControlHistoryWidget::GetDescription) ] ] ] ; } END_SLATE_FUNCTION_BUILD_OPTIMIZATION /** Get the last selected revision's revision number */ FText GetRevisionNumber() const { if(LastSelectedRevisionItem.IsValid()) { return FText::FromString(LastSelectedRevisionItem.Pin()->Revision); } return FText::GetEmpty(); } /** Get the last selected revision's date */ FText GetDate() const { if(LastSelectedRevisionItem.IsValid() && LastSelectedRevisionItem.Pin()->Date != 0) { return FText::AsDateTime(LastSelectedRevisionItem.Pin()->Date); } return FText::GetEmpty(); } /** Get the last selected revision's username */ FText GetUserName() const { if(LastSelectedRevisionItem.IsValid()) { return FText::FromString(LastSelectedRevisionItem.Pin()->UserName); } return FText::GetEmpty(); } /** Get the last selected revision's revision number */ FText GetAction() const { if(LastSelectedRevisionItem.IsValid()) { return FText::FromString(LastSelectedRevisionItem.Pin()->Action); } return FText::GetEmpty(); } /** Get the last selected revision's changelist number */ FText GetChangelistNumber() const { static const bool bUsesChangelists = ISourceControlModule::Get().GetProvider().UsesChangelists(); if(LastSelectedRevisionItem.IsValid() && bUsesChangelists) { // don't group the CL# as Perforce doesn't display it that way return FText::AsNumber(LastSelectedRevisionItem.Pin()->ChangelistNumber, &FNumberFormattingOptions::DefaultNoGrouping()); } return FText::GetEmpty(); } /** Get the last selected revision's client spec */ FText GetClientSpec() const { if(LastSelectedRevisionItem.IsValid()) { return FText::FromString(LastSelectedRevisionItem.Pin()->ClientSpec); } return FText::GetEmpty(); } /** Get the last selected revision's file size */ FText GetFileSize() const { if(LastSelectedRevisionItem.IsValid()) { static const FNumberFormattingOptions FileSizeFormatOptions = FNumberFormattingOptions() .SetMinimumFractionalDigits(1) .SetMaximumFractionalDigits(1); return FText::AsMemory(LastSelectedRevisionItem.Pin()->FileSize, &FileSizeFormatOptions); } return FText::GetEmpty(); } /** Get the last selected revision's description */ FText GetDescription() const { if(LastSelectedRevisionItem.IsValid()) { return FText::FromString(LastSelectedRevisionItem.Pin()->Description); } return FText::GetEmpty(); } /** Get the last selected revision's description */ FText GetBranchedFrom() const { if(LastSelectedRevisionItem.IsValid()) { return FText::FromString(LastSelectedRevisionItem.Pin()->BranchSource); } return FText::GetEmpty(); } /** * Generates the content of each row, displaying a the File or Revision data for its corresponding type */ TSharedRef OnGenerateRowForHistoryFileList( TSharedPtr TreeItemPtr, const TSharedRef& OwnerTable ) { TSharedPtr RowContent; if (TreeItemPtr->FileListItem.IsValid()) { TSharedPtr FileListItem = TreeItemPtr->FileListItem; FString FileName = FileListItem->FileName; FPaths::MakePlatformFilename(FileName); return SNew(STableRow< TSharedPtr >, OwnerTable) [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() .Padding(5) [ SNew( STextBlock ) .Font( FAppStyle::GetFontStyle( TEXT("BoldFont") )) .Text( FText::FromString(MoveTemp(FileName)) ) ] ] .OnDragDetected(this, &SSourceControlHistoryWidget::OnRowDragDetected) .OnDragEnter(this, &SSourceControlHistoryWidget::OnRowDragEnter, TreeItemPtr) .OnDragLeave(this, &SSourceControlHistoryWidget::OnRowDragLeave) .OnDrop(this, &SSourceControlHistoryWidget::OnRowDrop, TreeItemPtr); } else if (TreeItemPtr->RevisionListItem.IsValid()) { TSharedPtr RevisionListItem = TreeItemPtr->RevisionListItem; return SNew(SHistoryRevisionListRowContent, OwnerTable) .RevisionListItem(RevisionListItem) .OnDragDetected(this, &SSourceControlHistoryWidget::OnRowDragDetected) .OnDragEnter(this, &SSourceControlHistoryWidget::OnRowDragEnter, TreeItemPtr) .OnDragLeave(this, &SSourceControlHistoryWidget::OnRowDragLeave) .OnDrop(this, &SSourceControlHistoryWidget::OnRowDrop, TreeItemPtr) .HasChildren(TreeItemPtr->Children.Num() > 0); } //we should never get here... return SNew(STableRow< TSharedPtr >, OwnerTable) [ SNew( STextBlock ) .Text( NSLOCTEXT("SourceControlHistory", "ErrorMessage", "---ERROR---") ) ]; } /** * Fill out the tree structure with the SSC data */ void AddHistoryInfo( const TArray< FSourceControlStateRef >& InStates ) { // Construct a new observable collection to serve as the items source for the main list view. It will contain each history file item. for( TArray< FSourceControlStateRef >::TConstIterator Iter(InStates); Iter; Iter++) { FSourceControlStateRef SourceControlState = *Iter; TSharedPtr FileItem = MakeShareable(new FHistoryTreeItem()); FileItem->FileListItem = MakeShareable(new FHistoryFileListViewItem( SourceControlState->GetFilename() )); // Add each file revision for ( int32 HistoryIndex = 0; HistoryIndex < SourceControlState->GetHistorySize(); HistoryIndex++ ) { TSharedPtr Revision = SourceControlState->GetHistoryItem(HistoryIndex); check(Revision.IsValid()); TSharedPtr RevisionItem = MakeShareable(new FHistoryTreeItem()); RevisionItem->RevisionListItem = MakeShareable(new FHistoryRevisionListViewItem( Revision.ToSharedRef() )); FileItem->Children.Add(RevisionItem); RevisionItem->Parent = FileItem; // File Items other than the current workspace version masquarade as the latest version of that file if (HistoryIndex == 0 && HistoryCollection.Num() > 0) { FileItem->RevisionListItem = RevisionItem->RevisionListItem; } // add branch items if we have one if(Revision->GetBranchSource().IsValid()) { TSharedPtr BranchFileItem = MakeShareable(new FHistoryTreeItem()); const FString BranchRevisionName = FString::Printf(TEXT("%s #%d"), *Revision->GetBranchSource()->GetFilename(), Revision->GetBranchSource()->GetRevisionNumber()); BranchFileItem->FileListItem = MakeShareable(new FHistoryFileListViewItem( BranchRevisionName )); RevisionItem->Children.Add(BranchFileItem); BranchFileItem->Parent = RevisionItem; } } HistoryCollection.Add(FileItem); } } /** * Callback returns the revision history (Children) nodes for the file (InItem) node */ void OnGetChildrenForHistoryFileList( TSharedPtr< FHistoryTreeItem > InItem, TArray< TSharedPtr >& OutChildren ) { OutChildren = InItem->Children; } /** * Called whenever the IsSelected property on a MHistoryRevisionListViewItem changes. Used to specify the last selected revision item. * * @param Owner Object which triggered the event * @param Args Event arguments for the property change */ void OnRevisionPropertyChanged(TSharedPtr Item, ESelectInfo::Type SelectInfo) { LastSelectedRevisionItem.Reset(); if (Item.IsValid()) { if (Item->RevisionListItem.IsValid()) { LastSelectedRevisionItem = Item->RevisionListItem; } else if (Item->Children.Num() > 0 && Item->Children[0]->RevisionListItem.IsValid()) { LastSelectedRevisionItem = Item->Children[0]->RevisionListItem; } } AdditionalInfoItemsControl->SetContent(GetAdditionalInfoItemsControlContent()); } /** Called to create a context menu when right-clicking on a history item */ TSharedPtr< SWidget > OnCreateContextMenu() { FToolMenuContext Context; USourceControlHistoryWidgetContext* SourceControlHistoryWidgetContext = NewObject(); SourceControlHistoryWidgetContext->HistoryWidget = SharedThis(this); for (TSharedPtr Item : MainHistoryListView->GetSelectedItems()) { if (Item->Parent.IsValid()) { TSharedPtr Parent = Item->Parent.Pin(); USourceControlHistoryWidgetContext::SelectedItem ItemInfo; if (Parent->FileListItem) { ItemInfo.FileName = Parent->FileListItem->FileName; ItemInfo.Revision = Item->RevisionListItem->Revision; SourceControlHistoryWidgetContext->GetSelectedItems().Add(ItemInfo); } } } Context.AddObject(SourceControlHistoryWidgetContext); if (UToolMenu* GeneratedContextMenu = UToolMenus::Get()->GenerateMenu(SourceControlHistoryContextMenu, Context)) { return UToolMenus::Get()->GenerateWidget(GeneratedContextMenu); } return SNullWidget::NullWidget; } /** See if we should enabled the 'diff against previous' option */ bool CanDiffAgainstPreviousRev() const { TArray< TSharedPtr > SelectedRevs = MainHistoryListView->GetSelectedItems(); // Only allow option if we selected one item if (SelectedRevs.Num() == 1 && SelectedRevs[0].IsValid()) { TSharedPtr SelectedItem = SelectedRevs[0]; if (SelectedItem->Parent.IsValid()) { // If it is a revision then allow diffing against previous revision unless it is the original revision TSharedPtr FileItem = SelectedItem->Parent.Pin(); check(FileItem.IsValid()); // If we've got the expanded RevisionList entry rather than the FileList entry // move the Selected and FileItem up one level in the hierarchy if (FileItem->FileListItem == nullptr) { SelectedItem = FileItem; FileItem = FileItem->Parent.Pin(); check(FileItem.IsValid()) } int32 RevIndex = FileItem->Children.Find(SelectedItem); check(RevIndex != INDEX_NONE); if (RevIndex == FileItem->Children.Num()-1) // If oldest revision of this file { int32 FileIndex = HistoryCollection.Find(FileItem); check(FileIndex != INDEX_NONE); return (FileIndex < HistoryCollection.Num()-1); } } else { // If it is a file item then allow diffing against previous revision unless it is the last // file item and it only has one child const int32 FileIndex = HistoryCollection.Find(SelectedItem); if (FileIndex > 0 && FileIndex == HistoryCollection.Num() - 1) { return (SelectedItem->Children.Num() > 1); } } return true; } return false; } /** Try and perform a diff between the selected revision and the previous one */ void OnDiffAgainstPreviousRev() { TArray< TSharedPtr > SelectedRevs = MainHistoryListView->GetSelectedItems(); if(SelectedRevs.Num() > 0 && SelectedRevs[0].IsValid()) { FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(TEXT("AssetTools")); TSharedPtr SelectedItem = SelectedRevs[0]; // This might be the expanded RevisionList item, if so we need to push up one level in the hierarchy if (!SelectedItem->RevisionListItem.IsValid()) { TSharedPtr ParentItem = SelectedItem->Parent.Pin(); if (ParentItem.IsValid() && ParentItem->RevisionListItem.IsValid()) { SelectedItem = ParentItem; } } UObject* SelectedAsset = GetAssetRevisionObject(SelectedItem); if (SelectedItem->RevisionListItem.IsValid()) { TSharedPtr FileItem; int32 RevIndex; if (SelectedItem->Parent.IsValid()) { FileItem = SelectedItem->Parent.Pin(); check(FileItem.IsValid()); // First, find index of selected revision in its parent file item // NB. 0 is newest, increasing index means older RevIndex = FileItem->Children.Find(SelectedItem); check(RevIndex != INDEX_NONE); } else { // If the selected item doesn't have a parent, this is a file item // masquerading as the most recent revision FileItem = SelectedItem; RevIndex = 0; } // Now we need to find previous revision TSharedPtr PreRevisionItem; if(RevIndex == FileItem->Children.Num()-1) // If oldest revision of this file { // .. see if we have an older file int32 FileIndex = HistoryCollection.Find(FileItem); check(FileIndex != INDEX_NONE); // Do nothing if we selected the newest revision of the newest file... if(FileIndex < HistoryCollection.Num()-1) { // Previous revision is a different file, so get the newest revision of the older file TSharedPtr PrevFileItem = HistoryCollection[FileIndex+1]; check(PrevFileItem.IsValid()); if(PrevFileItem->Children.Num() > 0) { PreRevisionItem = PrevFileItem->Children[0]; } } } else { // Not the oldest revision of this file, grab the older entry and get revision number PreRevisionItem = FileItem->Children[RevIndex+1]; } UObject* PreviousAsset = GetAssetRevisionObject(PreRevisionItem); if ((SelectedAsset != NULL) && (PreviousAsset != NULL)) { FRevisionInfo OldRevisionInfo; GetRevisionInfo(PreRevisionItem, OldRevisionInfo); FRevisionInfo NewRevisionInfo; GetRevisionInfo(SelectedItem, NewRevisionInfo); AssetToolsModule.Get().DiffAssets(PreviousAsset, SelectedAsset, OldRevisionInfo, NewRevisionInfo); } else { FMessageDialog::Open( EAppMsgType::Ok, NSLOCTEXT("SourceControl.HistoryWindow", "UnableToLoadAssets", "Unable to load assets to diff. Content may no longer be supported?")); } } else if (SelectedAsset != NULL) { // this should be a file list-item (representing the current working version) check(SelectedItem->FileListItem.IsValid()); FString const AssetName = SelectedAsset->GetName(); FString PackageName; if (FPackageName::TryConvertFilenameToLongPackageName(SelectedItem->FileListItem->FileName, /*out*/ PackageName)) { AssetToolsModule.Get().DiffAgainstDepot(SelectedAsset, PackageName, AssetName); } } } } /** See if we should enabled the 'diff against workspace' option */ bool CanDiffAgainstWorkspace() const { // Only allow option if we selected one item and it is not the current workspace row TArray< TSharedPtr > SelectedRevs = MainHistoryListView->GetSelectedItems(); return (SelectedRevs.Num() == 1 && SelectedRevs[0].IsValid() && HistoryCollection[0] != SelectedRevs[0]); } /** Try and perform a diff between the selected revision and the current version of the asset */ void OnDiffAgainstWorkspace() { TArray< TSharedPtr > SelectedRevs = MainHistoryListView->GetSelectedItems(); if (SelectedRevs.Num() > 0 && SelectedRevs[0].IsValid()) { FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(TEXT("AssetTools")); TSharedPtr SelectedItem = SelectedRevs[0]; // This might be the expanded RevisionList item, if so we need to push up one level in the hierarchy if (!SelectedItem->RevisionListItem.IsValid()) { TSharedPtr ParentItem = SelectedItem->Parent.Pin(); if (ParentItem.IsValid() && ParentItem->RevisionListItem.IsValid()) { SelectedItem = ParentItem; } } UObject* SelectedAsset = GetAssetRevisionObject(SelectedItem); if (SelectedItem->RevisionListItem.IsValid()) { UObject* CurrentAsset = (HistoryCollection.Num() > 0 ? GetAssetRevisionObject(HistoryCollection[0]) : nullptr); if ((SelectedAsset != NULL) && (CurrentAsset != NULL)) { FRevisionInfo OldRevisionInfo; GetRevisionInfo(SelectedItem, OldRevisionInfo); FRevisionInfo NewRevisionInfo; NewRevisionInfo.Revision = TEXT(""); AssetToolsModule.Get().DiffAssets(SelectedAsset, CurrentAsset, OldRevisionInfo, NewRevisionInfo); } else { FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("SourceControl.HistoryWindow", "UnableToLoadAssets", "Unable to load assets to diff. Content may no longer be supported?")); } } else if (SelectedAsset != NULL) { // this should be a file list-item (representing the current working version) check(SelectedItem->FileListItem.IsValid()); FString const AssetName = SelectedAsset->GetName(); FString PackageName; if (FPackageName::TryConvertFilenameToLongPackageName(SelectedItem->FileListItem->FileName, /*out*/ PackageName)) { AssetToolsModule.Get().DiffAgainstDepot(SelectedAsset, PackageName, AssetName); } } } } /** * Checks to see if the SourceControl provider supports diffing against depot. * * @return True if the SourceControl provider supports diffing, false if not. */ static bool CanDiff() { return ISourceControlModule::Get().GetProvider().AllowsDiffAgainstDepot(); } /** * Checks to see if the selected history-tree items can be diffed against each other. * * @return True if the selected items can be diffed, false if not. */ bool CanDiffSelected() const { // throw away text so we can utilize a shared utility method FText ThrowAwayErrorText; TArray< TSharedPtr > SelectedRevs = MainHistoryListView->GetSelectedItems(); return CanDiffSelectedItems(SelectedRevs, ThrowAwayErrorText); } /** * Takes the two selected history items and finds a UObject asset for each, * then attempts to open a diff window to compare them. */ void OnDiffSelected() const { TArray< TSharedPtr > SelectedRevs = MainHistoryListView->GetSelectedItems(); if (SelectedRevs.Num() >= 2) { if (!DiffHistoryItems(SelectedRevs[0], SelectedRevs[1])) { FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("SourceControl.HistoryWindow", "UnableToLoadAssets", "Unable to load assets to diff. Content may no longer be supported?")); } } } /** * CanDiffSelected gives a dynamic response based on a number of factors, so CreateDiffSelectedMenu ensures we are checking on each menu open event and correctly adding the related menu item(s) for each case. */ void CreateDiffSelectedMenu(FToolMenuSection& InSection) { if (CanDiffSelected()) { InSection.AddMenuEntry( TEXT("DiffSelected"), NSLOCTEXT("SourceControl.HistoryWindow.Menu", "DiffSelected", "Diff Selected"), NSLOCTEXT("SourceControl.HistoryWindow.Menu", "DiffSelectedTooltip", "Diff the two assets that you have selected."), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SSourceControlHistoryWidget::OnDiffSelected) ) ); } } /** * An event handler for mouse drag detection events. Intended to be used as a delegate for * history tree rows. Creates a FSourceControlHistoryRowDragDropOp (if the drag was with * the left mouse-button) and assumes that all the selected items are the objects being dragged. * * @param MyGeometry The geometry for the dragged widget. * @param MouseEvent Describes the mouse drag action (from when the drag was detected). * @return A reply detailing how this event was handled ("Unhandled" if the click was not a left-click). */ FReply OnRowDragDetected(FGeometry const& MyGeometry, FPointerEvent const& MouseEvent) const { if (MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton)) //can only drag when editing { TSharedRef DragOperation = FSourceControlHistoryRowDragDropOp::New(); // assume that what we're dragging is what we have selected DragOperation->SelectedItems = MainHistoryListView->GetSelectedItems(); check(DragOperation->SelectedItems.Num() > 0); return FReply::Handled().BeginDragDrop(DragOperation); } else { return FReply::Unhandled(); } } /** * An event handler for mouse drag enter events. Intended to be used as a delegate for * history tree rows. Only handles FSourceControlHistoryRowDragDropOp drag/drop operations. * Updates the FSourceControlHistoryRowDragDropOp to reflect if a drop action is doable * (when over this item). * * @param DragDropEvent The drag/drop operation that triggered this handler. * @param HoveredItem The history tree item that is conceptually being hovered over. */ void OnRowDragEnter(FDragDropEvent const& DragDropEvent, TSharedPtr const HoveredItem) const { TSharedPtr DragRowOp = DragDropEvent.GetOperationAs(); if (DragRowOp.IsValid()) { DragRowOp->PendingDropAction = FSourceControlHistoryRowDragDropOp::EDropAction::None; TArray< TSharedPtr > DiffingItems = DragRowOp->SelectedItems; check(HoveredItem.IsValid()); DiffingItems.Add(HoveredItem); if (CanDiffSelectedItems(DiffingItems, DragRowOp->HoverText)) { DragRowOp->PendingDropAction = FSourceControlHistoryRowDragDropOp::EDropAction::Diff; FText const RevisionFormatText = NSLOCTEXT("SourceControlHistory", "Revision", "Revision {0}"); FText const CurrentRevisionText = NSLOCTEXT("SourceControlHistory", "CurrentRevsion", "Current Revision"); check(DragRowOp->SelectedItems.Num() > 0); TSharedPtr DraggedItem = DiffingItems[0]; // set text identifying the dragged item's revision (current version vs. revision X) FText DraggedRevisionText = CurrentRevisionText; if (DraggedItem->RevisionListItem.IsValid()) { DraggedRevisionText = FText::Format(RevisionFormatText, FText::FromString(DraggedItem->RevisionListItem->Revision)); } // set text identifying the hovered item's revision (current version vs. revision X) FText HoveredRevisionText = CurrentRevisionText; if (HoveredItem->RevisionListItem.IsValid()) { HoveredRevisionText = FText::Format(RevisionFormatText, FText::FromString(HoveredItem->RevisionListItem->Revision)); } TSharedPtr const DraggedFileItem = GetFileListItem(DraggedItem); // convert DraggedRevisionText from the form "revision X" (or "the current version") to // "revision X of " FString const DraggedFileName = FPaths::GetBaseFilename(DraggedFileItem->FileName); FText const NamedRevisionTextFormat = NSLOCTEXT("SourceControlHistory", "NamedRevision", "{0} ({1})"); DraggedRevisionText = FText::Format(NamedRevisionTextFormat, FText::FromString(DraggedFileName), DraggedRevisionText); TSharedPtr const HoveredFileItem = GetFileListItem(HoveredItem); // if we're diffing two separate files against each other if (DraggedFileItem != HoveredFileItem) { // need to separately identify the hovered over item FString const HoveredFileName = FPaths::GetBaseFilename(HoveredFileItem->FileName); HoveredRevisionText = FText::Format(NamedRevisionTextFormat, FText::FromString(HoveredFileName), HoveredRevisionText); } FText const DropToDiffTextFormat = NSLOCTEXT("SourceControlHistory", "DropToDiff", "Drop {0} to diff against: {1}."); DragRowOp->HoverText = FText::Format(DropToDiffTextFormat, DraggedRevisionText, HoveredRevisionText); } } } /** * An event handler for mouse drag leave events. Intended to be used as a delegate for * history tree rows. Only handles FSourceControlHistoryRowDragDropOp drag/drop operations. * Clears any pending drop actions from the FSourceControlHistoryRowDragDropOp. * * @param DragDropEvent The drag/drop operation that triggered this handler. */ void OnRowDragLeave(FDragDropEvent const& DragDropEvent) const { TSharedPtr DragRowOp = DragDropEvent.GetOperationAs(); if (DragRowOp.IsValid()) { DragRowOp->HoverText = FText::GetEmpty(); DragRowOp->PendingDropAction = FSourceControlHistoryRowDragDropOp::EDropAction::None; } } /** * An event handler for mouse drop events. Intended to be used as a delegate for * history tree rows. Only handles FSourceControlHistoryRowDragDropOp drag/drop operations. * Executes the pending FSourceControlHistoryRowDragDropOp action (diff, etc.) * * @param DragDropEvent The drag/drop operation that triggered this handler. * @param HoveredItem The history tree item that is conceptually being dropped on to. * @return A reply detailing how this event was handled ("Unhandled" if the operation was not a FSourceControlHistoryRowDragDropOp). */ FReply OnRowDrop(FDragDropEvent const& DragDropEvent, TSharedPtr const HoveredItem) const { TSharedPtr DragRowOp = DragDropEvent.GetOperationAs(); if (DragRowOp.IsValid()) { if (DragRowOp->PendingDropAction == FSourceControlHistoryRowDragDropOp::EDropAction::Diff) { check(DragRowOp->SelectedItems.Num() > 0); DiffHistoryItems(DragRowOp->SelectedItems[0], HoveredItem); return FReply::Handled(); } } return FReply::Unhandled(); } /** Main list view of the panel, displays each file history item */ typedef STreeView< TSharedPtr > SHistoryFileListType; /** ListBox for selecting which object to consolidate */ TSharedPtr< SHistoryFileListType > MainHistoryListView; /** Items control for the "additional information" subpanel */ TSharedPtr AdditionalInfoItemsControl; /** All file history items the panel should display */ TArray< TSharedPtr > HistoryCollection; /** The last selected revision item; Displayed in the "additional information" subpanel */ TWeakPtr LastSelectedRevisionItem; /** Pointer to the parent window */ TWeakPtr ParentWindow; }; void SSourceControlHistoryWidget::CreateDiffMenu(UToolMenu* InToolMenu) { USourceControlHistoryWidgetContext* FoundContext = InToolMenu->FindContext(); if (!FoundContext) { return; } TSharedPtr PinnedHistoryWidget = FoundContext->HistoryWidget.Pin(); if (!PinnedHistoryWidget) { return; } if (!CanDiff()) { return; } FToolMenuSection& DiffSection = InToolMenu->AddSection("DiffTools"); DiffSection.AddMenuEntry( TEXT("DiffAgainstPrevious"), NSLOCTEXT("SourceControl.HistoryWindow.Menu", "DiffAgainstPrev", "Diff Against Previous Revision"), NSLOCTEXT("SourceControl.HistoryWindow.Menu", "DiffAgainstPrevTooltip", "See changes between this revision and the previous one."), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(PinnedHistoryWidget.Get(), &SSourceControlHistoryWidget::OnDiffAgainstPreviousRev), FCanExecuteAction::CreateSP(PinnedHistoryWidget.Get(), &SSourceControlHistoryWidget::CanDiffAgainstPreviousRev) ) ); DiffSection.AddMenuEntry( TEXT("DiffAgainstWorkspace"), NSLOCTEXT("SourceControl.HistoryWindow.Menu", "DiffAgainstWorkspace", "Diff Against Workspace File"), NSLOCTEXT("SourceControl.HistoryWindow.Menu", "DiffAgainstWorkspaceTooltip", "See changes between this revision and your version of the asset."), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(PinnedHistoryWidget.Get(), &SSourceControlHistoryWidget::OnDiffAgainstWorkspace), FCanExecuteAction::CreateSP(PinnedHistoryWidget.Get(), &SSourceControlHistoryWidget::CanDiffAgainstWorkspace) ) ); DiffSection.AddDynamicEntry(NAME_None, FNewToolMenuSectionDelegate::CreateSP(PinnedHistoryWidget.Get(), &SSourceControlHistoryWidget::CreateDiffSelectedMenu)); } void FSourceControlWindows::DisplayRevisionHistory( const TArray& InPackageNames ) { // Explicitly load the module so live coding will work with it FModuleManager::Get().LoadModule(TEXT("SourceControlWindows")); ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); // Query for the file history for the provided packages TArray PackageFilenames = SourceControlHelpers::PackageFilenames(InPackageNames); TSharedRef UpdateStatusOperation = ISourceControlOperation::Create(); UpdateStatusOperation->SetUpdateHistory(true); if(SourceControlProvider.Execute(UpdateStatusOperation, PackageFilenames)) { TArray< FSourceControlStateRef > SourceControlStates; for(const FString& PackageFilename : PackageFilenames) { TArray RevisionName; RevisionName.Add(PackageFilename); while(RevisionName.Num() != 0) { int32 InitialNum = SourceControlStates.Num(); SourceControlProvider.GetState(RevisionName, SourceControlStates, EStateCacheUsage::Use); int32 NewNum = SourceControlStates.Num(); ensure(NewNum >= InitialNum); RevisionName.Empty(); // check to see if origin of this file is a branch, append the history from the branch point: if(NewNum > InitialNum) { int32 HistorySize = SourceControlStates.Last()->GetHistorySize(); if( HistorySize > 0 ) { TSharedPtr InitialHistory = SourceControlStates.Last()->GetHistoryItem(HistorySize - 1); TSharedPtr BranchSource = InitialHistory->GetBranchSource(); if( BranchSource.IsValid() ) { RevisionName.Add(BranchSource->GetFilename()); } } } } } TSharedRef NewWindow = SNew(SWindow) .Title( NSLOCTEXT("SourceControl.HistoryWindow", "Title", "File History") ) .SizingRule(ESizingRule::UserSized) .AutoCenter(EAutoCenter::PreferredWorkArea) .ClientSize(FVector2D(1000, 400)); TSharedRef SourceControlWidget = SNew(SSourceControlHistoryWidget) .ParentWindow(NewWindow) .SourceControlStates(SourceControlStates); NewWindow->SetContent( SourceControlWidget ); TSharedPtr RootWindow = FGlobalTabmanager::Get()->GetRootWindow(); if(RootWindow.IsValid()) { FSlateApplication::Get().AddWindowAsNativeChild(NewWindow, RootWindow.ToSharedRef()); } else { FSlateApplication::Get().AddWindow(NewWindow); } } } bool FSourceControlWindows::DiffAgainstWorkspace(const FString& InFileName) { FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(TEXT("AssetTools")); UObject* SelectedAsset = nullptr; FString AssetPackageName; if (FPackageName::TryConvertFilenameToLongPackageName(InFileName, AssetPackageName)) { UPackage* AssetPackage = LoadPackage(nullptr, *AssetPackageName, LOAD_None); // grab the asset from the package - we assume asset name matches file name FString AssetName = FPaths::GetBaseFilename(InFileName); SelectedAsset = FindObject(AssetPackage, *AssetName); if (!SelectedAsset && AssetPackage) { SelectedAsset = AssetPackage->FindAssetInPackage(); } } if (SelectedAsset) { AssetToolsModule.Get().DiffAgainstDepot(SelectedAsset, AssetPackageName, SelectedAsset->GetName()); } return !!SelectedAsset; } bool FSourceControlWindows::DiffAgainstShelvedFile(const FSourceControlStateRef& InFileState) { if (InFileState->GetHistorySize() == 0) { return false; } TSharedPtr Revision = InFileState->GetHistoryItem(0); check(Revision.IsValid()); UObject* SelectedAsset = nullptr; FString AssetPackageName; if (FPackageName::TryConvertFilenameToLongPackageName(InFileState->GetFilename(), AssetPackageName)) { UPackage* AssetPackage = LoadPackage(nullptr, *AssetPackageName, LOAD_None); // grab the asset from the package - we assume asset name matches file name FString AssetName = FPaths::GetBaseFilename(InFileState->GetFilename()); SelectedAsset = FindObject(AssetPackage, *AssetName); if (!SelectedAsset && AssetPackage) { SelectedAsset = AssetPackage->FindAssetInPackage(); } } if (SelectedAsset) { FString TempFileName; if (Revision->Get(TempFileName)) { // Try and load that package const FPackagePath TempPackagePath = FPackagePath::FromLocalPath(TempFileName); const FPackagePath OriginalPackagePath = FPackagePath::FromLocalPath(InFileState->GetFilename()); if(UPackage* TempPackage = DiffUtils::LoadPackageForDiff(TempPackagePath, OriginalPackagePath)) { // Grab the shelved asset from that package UObject* ShelvedObject = FindObject(TempPackage, *SelectedAsset->GetName()); if (ShelvedObject == nullptr) { ShelvedObject = TempPackage->FindAssetInPackage(); } if (ShelvedObject != nullptr) { /* Set the revision information*/ FRevisionInfo ShelvedRevision; ShelvedRevision.Changelist = Revision->GetCheckInIdentifier(); ShelvedRevision.Revision = TEXT("Shelved"); ShelvedRevision.Date = Revision->GetDate(); FRevisionInfo NewRevision; NewRevision.Revision = TEXT(""); FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(TEXT("AssetTools")); AssetToolsModule.Get().DiffAssets(ShelvedObject, SelectedAsset, ShelvedRevision, NewRevision); } } } } return !!SelectedAsset; }