Files
UnrealEngine/Engine/Source/Editor/SourceControlWindows/Private/SSourceControlRevert.cpp
2025-05-18 13:04:45 +08:00

697 lines
21 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "CoreMinimal.h"
#include "AssetToolsModule.h"
#include "Misc/MessageDialog.h"
#include "IAssetTools.h"
#include "ISourceControlOperation.h"
#include "RevisionControlStyle/RevisionControlStyle.h"
#include "SourceControlOperations.h"
#include "SourceControlWindows.h"
#include "SourceControlHelpers.h"
#include "SSourceControlChangelistRows.h"
#include "ISourceControlModule.h"
#include "UObject/Package.h"
#include "Misc/PackageName.h"
#include "Layout/Visibility.h"
#include "Input/Reply.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/SCompoundWidget.h"
#include "Widgets/SBoxPanel.h"
#include "Styling/SlateTypes.h"
#include "Widgets/SWindow.h"
#include "SlateOptMacros.h"
#include "Framework/Application/SlateApplication.h"
#include "Widgets/Layout/SBorder.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Views/STableViewBase.h"
#include "Widgets/Views/STableRow.h"
#include "Widgets/Views/SListView.h"
#include "Widgets/Input/SCheckBox.h"
#include "Styling/AppStyle.h"
#include "PackageTools.h"
#include "Settings/EditorExperimentalSettings.h"
#include "Framework/Notifications/NotificationManager.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "UObject/Linker.h"
#include "FileHelpers.h"
#define LOCTEXT_NAMESPACE "SSourceControlRevert"
//-------------------------------------
//Source Control Window Constants
//-------------------------------------
enum class ERevertResults
{
Accepted,
Canceled
};
/** Additional state for each IFileViewTreeItem row. */
struct FRevertTableRowState
{
FRevertTableRowState(const FString& InPackageName, bool bInIsModified)
: bIsModified(bInIsModified)
, PackageName(InPackageName)
{
}
const bool bIsModified;
const FString PackageName;
};
/** Row widget - derives from either SFileTableRow or SOfflineFileTableRow depending on whether we are performing a unsaved revert or not. */
template<typename CommonTableRowType>
class SSourceControlRevertRow : public CommonTableRowType
{
public:
SLATE_BEGIN_ARGS(SSourceControlRevertRow)
: _ShowingContentVersePath(false)
{
}
SLATE_ARGUMENT(FChangelistTreeItemPtr, TreeItemToVisualize)
SLATE_ARGUMENT(bool, IsModified)
SLATE_ARGUMENT(bool, ShowingContentVersePath)
SLATE_END_ARGS()
public:
void Construct(const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwner)
{
bIsModified = InArgs._IsModified;
CommonTableRowType::Construct(
typename CommonTableRowType::FArguments()
.TreeItemToVisualize(InArgs._TreeItemToVisualize)
.PathFlags(InArgs._ShowingContentVersePath ? SourceControlFileViewColumn::EPathFlags::ShowingVersePath : SourceControlFileViewColumn::EPathFlags::Default),
InOwner);
}
TSharedRef<SWidget> GenerateWidgetForColumn(const FName& ColumnName) override
{
if (ColumnName == SourceControlFileViewColumn::Name::Id() && bIsModified)
{
// If the item is modified, wrap the name widget with the modified icon.
return SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.HAlign(HAlign_Left)
.AutoWidth()
[
CommonTableRowType::GenerateWidgetForColumn(ColumnName)
]
+SHorizontalBox::Slot()
.HAlign(HAlign_Right)
.VAlign(VAlign_Center)
[
SNew(SImage)
.Image(FAppStyle::GetBrush(TEXT("ContentBrowser.ContentDirty")))
.ToolTipText(LOCTEXT("ModifiedFileToolTip", "This file has been modified from the source version"))
];
}
return CommonTableRowType::GenerateWidgetForColumn(ColumnName);
}
private:
bool bIsModified = false;
};
/** Returns whether revert unsaved is enabled */
static bool IsRevertUnsavedEnabled()
{
if (IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(TEXT("SourceControl.RevertUnsaved.Enable")))
{
return CVar->GetBool();
}
else
{
return false;
}
}
/**
* Source control panel for reverting files. Allows the user to select which files should be reverted, as well as
* provides the option to only allow unmodified files to be reverted.
*/
class SSourceControlRevertWidget : public SCompoundWidget
{
public:
//* @param InXamlName Name of the XAML file defining this panel
//* @param InPackageNames Names of the packages to be potentially reverted
SLATE_BEGIN_ARGS( SSourceControlRevertWidget )
: _ParentWindow()
, _PackagesToRevert()
{}
SLATE_ATTRIBUTE( TSharedPtr<SWindow>, ParentWindow )
SLATE_ATTRIBUTE( TArray<FString>, PackagesToRevert )
SLATE_END_ARGS()
/**
* Constructor.
*/
SSourceControlRevertWidget()
{
}
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void Construct( const FArguments& InArgs )
{
ParentFrame = InArgs._ParentWindow.Get();
bShowingContentVersePath = FAssetToolsModule::GetModule().Get().ShowingContentVersePath();
InitializeListViewItemSource(InArgs._PackagesToRevert.Get());
SortListViewItemSource();
TSharedRef<SHeaderRow> HeaderRowWidget = SNew(SHeaderRow);
ChildSlot
[
SNew(SBorder)
.BorderImage(FAppStyle::Get().GetBrush("Brushes.Panel"))
.Padding(FMargin(16))
[
SNew(SVerticalBox)
+SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 0.0f, 0.0f, 8.0f)
[
SNew(STextBlock)
.Text(NSLOCTEXT("SourceControl.Revert", "SelectFiles", "Select the files that should be reverted below"))
]
+SVerticalBox::Slot()
.FillHeight(1.0f)
[
SAssignNew(RevertListView, SListViewType)
.HeaderRow
(
SNew(SHeaderRow)
+SHeaderRow::Column(SourceControlFileViewColumn::CheckBox::Id())
.FixedWidth(38.0f)
[
SNew(SBox)
.Padding(FMargin(6.0f, 3.0f, 6.0f, 3.0f))
.HAlign(HAlign_Center)
[
SNew(SCheckBox)
.IsChecked(this, &SSourceControlRevertWidget::OnGetColumnHeaderState)
.IsEnabled(this, &SSourceControlRevertWidget::OnGetItemsEnabled)
.OnCheckStateChanged(this, &SSourceControlRevertWidget::ColumnHeaderClicked)
]
]
+ SHeaderRow::Column(SourceControlFileViewColumn::Icon::Id())
.DefaultTooltip(SourceControlFileViewColumn::Icon::GetToolTipText())
.FillSized(18.0f)
.HeaderContentPadding(FMargin(0.0f))
.SortMode(this, &SSourceControlRevertWidget::GetColumnSortMode, SourceControlFileViewColumn::Icon::Id())
.OnSort(this, &SSourceControlRevertWidget::OnColumnSortModeChanged)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.Padding(1.0f, 0.0f)
[
SNew(SBox)
.WidthOverride(16.0f)
.HeightOverride(16.0f)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.Visibility(this, &SSourceControlRevertWidget::GetIconColumnContentVisibility)
[
SNew(SImage)
.ColorAndOpacity(FSlateColor::UseSubduedForeground())
.Image(FRevisionControlStyleManager::Get().GetBrush("RevisionControl.Icon"))
]
]
]
+SHeaderRow::Column(SourceControlFileViewColumn::Name::Id())
.DefaultLabel(LOCTEXT("Asset", "Asset"))
.DefaultTooltip(SourceControlFileViewColumn::Name::GetToolTipText())
.FillWidth(5.0f)
.SortMode(this, &SSourceControlRevertWidget::GetColumnSortMode, SourceControlFileViewColumn::Name::Id())
.OnSort(this, &SSourceControlRevertWidget::OnColumnSortModeChanged)
+SHeaderRow::Column(SourceControlFileViewColumn::Path::Id())
.DefaultLabel(LOCTEXT("File", "File"))
.DefaultTooltip(SourceControlFileViewColumn::Path::GetToolTipText())
.FillWidth(7.0f)
.SortMode(this, &SSourceControlRevertWidget::GetColumnSortMode, SourceControlFileViewColumn::Path::Id())
.OnSort(this, &SSourceControlRevertWidget::OnColumnSortModeChanged)
+SHeaderRow::Column(SourceControlFileViewColumn::Type::Id())
.DefaultLabel(SourceControlFileViewColumn::Type::GetDisplayText())
.DefaultTooltip(SourceControlFileViewColumn::Type::GetToolTipText())
.FillWidth(2.0f)
.SortMode(this, &SSourceControlRevertWidget::GetColumnSortMode, SourceControlFileViewColumn::Type::Id())
.OnSort(this, &SSourceControlRevertWidget::OnColumnSortModeChanged)
)
.ListItemsSource(&ListViewItemSource)
.SelectionMode(ESelectionMode::None)
.OnGenerateRow(this, &SSourceControlRevertWidget::OnGenerateRowForList)
]
+SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 16.0f, 0.0f, 0.0f)
.VAlign(VAlign_Bottom)
.HAlign(HAlign_Fill)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
.Padding(5.0f, 0.0f)
.HAlign(HAlign_Left)
[
SNew(SCheckBox)
.OnCheckStateChanged(this, &SSourceControlRevertWidget::RevertUnchangedToggled)
[
SNew(STextBlock)
.Text(NSLOCTEXT("SourceControl.Revert", "RevertUnchanged", "Revert Unchanged Only"))
]
]
+SHorizontalBox::Slot()
.HAlign(HAlign_Right)
.FillWidth(1)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
.Padding(5.0f, 0.0f)
[
SNew(SButton)
.ButtonStyle(&FAppStyle::Get(), "PrimaryButton")
.TextStyle(&FAppStyle::Get(), "PrimaryButtonText")
.HAlign(HAlign_Center)
.OnClicked(this, &SSourceControlRevertWidget::OKClicked)
.IsEnabled(this, &SSourceControlRevertWidget::IsOKEnabled)
.Text(this, &SSourceControlRevertWidget::GetOkText)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(5.0f, 0.0f)
[
SNew(SButton)
.HAlign(HAlign_Center)
.OnClicked(this, &SSourceControlRevertWidget::CancelClicked)
.Text(LOCTEXT("CancelButton", "Cancel"))
]
]
]
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
/**
* Populates the provided array with the names of the packages the user elected to revert, if any.
*
* @param OutPackagesToRevert Array of package names to revert, as specified by the user in the dialog
*/
void GetPackagesToRevert( TArray<FString>& OutPackagesToRevert )
{
if (bRevertUnchangedFilesOnly)
{
for (const TPair<FChangelistTreeItemPtr, TSharedRef<FRevertTableRowState>>& Pair : ListViewItemState)
{
if (!Pair.Value->bIsModified)
{
OutPackagesToRevert.Add(Pair.Value->PackageName);
}
}
}
else
{
for (const FChangelistTreeItemPtr& Item : ListViewItemSource)
{
if (static_cast<IFileViewTreeItem*>(Item.Get())->GetCheckBoxState() == ECheckBoxState::Checked)
{
const TSharedRef<FRevertTableRowState>* State = ListViewItemState.Find(Item);
if (ensure(State))
{
OutPackagesToRevert.Add((*State)->PackageName);
}
}
}
}
}
ERevertResults GetResult()
{
return DialogResult;
}
private:
EColumnSortMode::Type GetColumnSortMode(FName ColumnId) const
{
if (SortByColumn != ColumnId)
{
return EColumnSortMode::None;
}
return SortMode;
}
void OnColumnSortModeChanged(EColumnSortPriority::Type SortPriority, const FName& ColumnId, EColumnSortMode::Type InSortMode)
{
SortByColumn = ColumnId;
SortMode = InSortMode;
SortListViewItemSource();
RevertListView->RequestListRefresh();
}
EVisibility GetIconColumnContentVisibility() const
{
// Hide the icon when sorting the icon column (it clashes with the sort mode icon).
return GetColumnSortMode(SourceControlFileViewColumn::Icon::Id()) == EColumnSortMode::None ? EVisibility::Visible : EVisibility::Collapsed;
}
TSharedRef<ITableRow> OnGenerateRowForList(FChangelistTreeItemPtr ListItemPtr, const TSharedRef<STableViewBase>& OwnerTable)
{
TSharedRef<FRevertTableRowState> State = ListViewItemState.FindChecked(ListItemPtr);
switch (ListItemPtr->GetTreeItemType())
{
case IChangelistTreeItem::File:
return SNew(SSourceControlRevertRow<SFileTableRow>, OwnerTable)
.IsEnabled(this, &SSourceControlRevertWidget::OnGetItemsEnabled)
.TreeItemToVisualize(ListItemPtr)
.IsModified(State->bIsModified)
.ShowingContentVersePath(bShowingContentVersePath);
case IChangelistTreeItem::OfflineFile:
return SNew(SSourceControlRevertRow<SOfflineFileTableRow>, OwnerTable)
.IsEnabled(this, &SSourceControlRevertWidget::OnGetItemsEnabled)
.TreeItemToVisualize(ListItemPtr)
.IsModified(State->bIsModified)
.ShowingContentVersePath(bShowingContentVersePath);
default:
checkNoEntry();
return SNew(STableRow<TSharedPtr<FChangelistTreeItemPtr>>, OwnerTable);
}
}
/** Called when the settings of the dialog are to be accepted*/
FReply OKClicked()
{
DialogResult = ERevertResults::Accepted;
ParentFrame.Pin()->RequestDestroyWindow();
return FReply::Handled();
}
bool IsOKEnabled() const
{
if (bRevertUnchangedFilesOnly)
{
for (const TPair<FChangelistTreeItemPtr, TSharedRef<FRevertTableRowState>>& Pair : ListViewItemState)
{
if (!Pair.Value->bIsModified)
{
return true;
}
}
}
else
{
for (const FChangelistTreeItemPtr& Item : ListViewItemSource)
{
if (static_cast<IFileViewTreeItem*>(Item.Get())->GetCheckBoxState() == ECheckBoxState::Checked)
{
return true;
}
}
}
return false;
}
FText GetOkText() const
{
if (bRevertUnchangedFilesOnly)
{
return LOCTEXT("RevertUnchangedButton", "Revert Unchanged");
}
return LOCTEXT("RevertButton", "Revert Selected");
}
/** Called when the settings of the dialog are to be ignored*/
FReply CancelClicked()
{
DialogResult = ERevertResults::Canceled;
ParentFrame.Pin()->RequestDestroyWindow();
return FReply::Handled();
}
/** Called when the user checks or unchecks the revert unchanged checkbox; updates the list view accordingly */
void RevertUnchangedToggled( const ECheckBoxState NewCheckedState )
{
bRevertUnchangedFilesOnly = (NewCheckedState == ECheckBoxState::Checked);
}
ECheckBoxState OnGetColumnHeaderState() const
{
int32 NumChecked = 0;
for (const FChangelistTreeItemPtr& Item : ListViewItemSource)
{
switch (static_cast<IFileViewTreeItem*>(Item.Get())->GetCheckBoxState())
{
case ECheckBoxState::Checked:
++NumChecked;
break;
case ECheckBoxState::Undetermined:
return ECheckBoxState::Undetermined;
}
}
if (NumChecked == 0)
{
return ECheckBoxState::Unchecked;
}
if (NumChecked == ListViewItemSource.Num())
{
return ECheckBoxState::Checked;
}
return ECheckBoxState::Undetermined;
}
/**
* Called whenever a column header is clicked, or in the case of the dialog, also when the "Check/Uncheck All" column header
* checkbox is called, because its event bubbles to the column header.
*/
void ColumnHeaderClicked( const ECheckBoxState NewCheckedState )
{
for (const FChangelistTreeItemPtr& Item : ListViewItemSource)
{
static_cast<IFileViewTreeItem*>(Item.Get())->SetCheckBoxState(NewCheckedState);
}
}
/** Initializes the current state of the files, */
void InitializeListViewItemSource(const TArray<FString>& PackagesToRevert)
{
const bool bRevertUnsaved = IsRevertUnsavedEnabled();
TArray<FString> PackageFilenames = SourceControlHelpers::PackageFilenames(PackagesToRevert);
// Make sure we update the modified state of the files
TSharedRef<FUpdateStatus, ESPMode::ThreadSafe> UpdateStatusOperation = ISourceControlOperation::Create<FUpdateStatus>();
UpdateStatusOperation->SetUpdateModifiedState(true);
ISourceControlModule::Get().GetProvider().Execute(UpdateStatusOperation, PackageFilenames);
// Find the files modified from the server version
TArray<FSourceControlStateRef> SourceControlStates;
if (ISourceControlModule::Get().GetProvider().GetState(PackageFilenames, SourceControlStates, EStateCacheUsage::Use))
{
ListViewItemSource.Reserve(SourceControlStates.Num());
for (const FSourceControlStateRef& SourceControlState : SourceControlStates)
{
FString PackageName;
if (ensure(FPackageName::TryConvertFilenameToLongPackageName(SourceControlState->GetFilename(), PackageName)))
{
const FChangelistTreeItemPtr& ListViewItem = ListViewItemSource.Add_GetRef(MakeShared<FFileTreeItem>(SourceControlState));
bool bIsModified = SourceControlState->IsModified();
if (bRevertUnsaved && !bIsModified)
{
if (UPackage* Package = FindPackage(nullptr, *PackageName))
{
// If the package contains unsaved changes, it's considered modified as well.
bIsModified = Package->IsDirty();
}
}
ListViewItemState.Emplace(ListViewItem, MakeShared<FRevertTableRowState>(PackageName, bIsModified));
}
}
}
else if (bRevertUnsaved)
{
ListViewItemSource.Reserve(PackagesToRevert.Num());
for (int32 Index = 0; Index < PackagesToRevert.Num(); ++Index)
{
const FChangelistTreeItemPtr& ListViewItem = ListViewItemSource.Add_GetRef(MakeShared<FOfflineFileTreeItem>(PackageFilenames[Index]));
bool bIsModified = false;
if (UPackage* Package = FindPackage(nullptr, *PackagesToRevert[Index]))
{
// If the package contains unsaved changes, it's considered modified.
bIsModified = Package->IsDirty();
}
ListViewItemState.Emplace(ListViewItem, MakeShared<FRevertTableRowState>(PackagesToRevert[Index], bIsModified));
}
}
}
void SortListViewItemSource()
{
TFunction<bool(const IFileViewTreeItem&, const IFileViewTreeItem&)> SortPredicate = SourceControlFileViewColumn::GetSortPredicate(
SortMode, SortByColumn, bShowingContentVersePath ? SourceControlFileViewColumn::EPathFlags::ShowingVersePath : SourceControlFileViewColumn::EPathFlags::Default);
if (SortPredicate)
{
Algo::SortBy(
ListViewItemSource,
[](const FChangelistTreeItemPtr& ListViewItem) -> const IFileViewTreeItem&
{
return static_cast<IFileViewTreeItem&>(*ListViewItem);
},
SortPredicate);
}
}
/** Check for whether the list items are enabled or not */
bool OnGetItemsEnabled() const
{
return !bRevertUnchangedFilesOnly;
}
TWeakPtr<SWindow> ParentFrame;
ERevertResults DialogResult = ERevertResults::Canceled;
/** ListView for the packages the user can revert */
typedef SListView<FChangelistTreeItemPtr> SListViewType;
TSharedPtr<SListViewType> RevertListView;
FName SortByColumn = SourceControlFileViewColumn::Name::Id();
EColumnSortMode::Type SortMode = EColumnSortMode::Ascending;
/** Collection of items serving as the data source for the list view */
TArray<FChangelistTreeItemPtr> ListViewItemSource;
TMap<FChangelistTreeItemPtr, TSharedRef<FRevertTableRowState>> ListViewItemState;
bool bShowingContentVersePath = false;
/** Flag set by the user to only revert non modified files */
bool bRevertUnchangedFilesOnly = false;
};
bool FSourceControlWindows::PromptForRevert( const TArray<FString>& InPackageNames, bool bInReloadWorld)
{
bool bReverted = false;
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
// Only add packages that can actually be reverted
TArray<FString> InitialPackagesToRevert;
for ( TArray<FString>::TConstIterator PackageIter( InPackageNames ); PackageIter; ++PackageIter )
{
FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(SourceControlHelpers::PackageFilename(*PackageIter), EStateCacheUsage::Use);
if( SourceControlState.IsValid() && SourceControlState->CanRevert() )
{
InitialPackagesToRevert.Add( *PackageIter );
}
else if ( IsRevertUnsavedEnabled() )
{
if (UPackage* Package = FindPackage(NULL, **PackageIter))
{
if (Package->IsDirty())
{
InitialPackagesToRevert.Add(*PackageIter);
}
}
}
}
// If any of the packages can be reverted, provide the revert prompt
if (InitialPackagesToRevert.Num() > 0)
{
TSharedRef<SWindow> NewWindow = SNew(SWindow)
.Title( NSLOCTEXT("SourceControl.RevertWindow", "Title", "Revert Files") )
.ClientSize(FVector2D(640.0f, 492.0f))
.SupportsMinimize(false)
.SupportsMaximize(false);
TSharedRef<SSourceControlRevertWidget> SourceControlWidget =
SNew(SSourceControlRevertWidget)
.ParentWindow(NewWindow)
.PackagesToRevert(InitialPackagesToRevert);
NewWindow->SetContent(SourceControlWidget);
FSlateApplication::Get().AddModalWindow(NewWindow, NULL);
// If the user decided to revert some packages, go ahead and do revert the ones they selected
if ( SourceControlWidget->GetResult() == ERevertResults::Accepted)
{
TArray<FString> FinalPackagesToRevert;
SourceControlWidget->GetPackagesToRevert(FinalPackagesToRevert);
if ( IsRevertUnsavedEnabled() )
{
// Unsaved changes need to be saved to disk so SourceControl realizes that there's something to revert.
TArray<UPackage*> FinalPackagesToSave;
for (const FString& PackageName : FinalPackagesToRevert)
{
if (UPackage* Package = FindPackage(NULL, *PackageName))
{
if (Package->IsDirty())
{
FinalPackagesToSave.Add(Package);
}
}
}
if (FinalPackagesToSave.Num() > 0)
{
UEditorLoadingAndSavingUtils::SavePackages(FinalPackagesToSave, /*bOnlyDirty=*/false);
}
}
if (FinalPackagesToRevert.Num() > 0)
{
SourceControlHelpers::RevertAndReloadPackages(FinalPackagesToRevert, /*bRevertAll=*/false, /*bReloadWorld=*/bInReloadWorld);
bReverted = true;
}
}
}
return bReverted;
}
bool FSourceControlWindows::RevertAllChangesAndReloadWorld()
{
return SourceControlHelpers::RevertAllChangesAndReloadWorld();
}
#undef LOCTEXT_NAMESPACE