// Copyright Epic Games, Inc. All Rights Reserved. #include "SSourceControlChangelists.h" #include "Editor.h" #include "Styling/AppStyle.h" #include "Algo/Transform.h" #include "Logging/MessageLog.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SSearchBox.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SSpacer.h" #include "Widgets/Layout/SSplitter.h" #include "Widgets/Layout/SExpandableArea.h" #include "Widgets/Layout/SWidgetSwitcher.h" #include "IAssetTools.h" #include "ISourceControlProvider.h" #include "ISourceControlModule.h" #include "ISourceControlWindowsModule.h" #include "Interfaces/IPluginManager.h" #include "UncontrolledChangelistsModule.h" #include "SourceControlOperations.h" #include "SSourceControlChangelistRows.h" #include "SSourceControlDescription.h" #include "SourceControlWindows.h" #include "SourceControlHelpers.h" #include "SourceControlFileStatusMonitor.h" #include "SourceControlPreferences.h" #include "SourceControlMenuContext.h" #include "SourceControlSettings.h" #include "AssetToolsModule.h" #include "ComponentReregisterContext.h" #include "FileHelpers.h" #include "Misc/CString.h" #include "Misc/MessageDialog.h" #include "Misc/PackageName.h" #include "Misc/ScopedSlowTask.h" #include "PackageSourceControlHelper.h" #include "Algo/AnyOf.h" #include "HAL/PlatformTime.h" #include "HAL/FileManager.h" #include "ToolMenus.h" #include "UnsavedAssetsTrackerModule.h" #include "UObject/UObjectGlobals.h" #include "UObject/Package.h" #include "SSourceControlSubmit.h" #include "Framework/Application/SlateApplication.h" #include "Framework/Notifications/NotificationManager.h" #include "Framework/Docking/TabManager.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "ProfilingDebugging/CpuProfilerTrace.h" #include "RevisionControlStyle/RevisionControlStyle.h" #define LOCTEXT_NAMESPACE "SourceControlChangelist" namespace { bool AreUnsavedAssetsPresent() { return FUnsavedAssetsTrackerModule::Get().GetUnsavedAssetNum() > 0; } /** Returns true if a source control provider is enable and support changeslists. */ bool AreControlledChangelistsEnabled() { return ISourceControlModule::Get().IsEnabled() && ISourceControlModule::Get().GetProvider().UsesChangelists(); }; /** Returns true if Uncontrolled changelists are enabled. */ bool AreUncontrolledChangelistsEnabled() { return FUncontrolledChangelistsModule::Get().IsEnabled(); }; /** Returns true if there are changelists to display. */ bool AreChangelistsEnabled() { return AreControlledChangelistsEnabled() || AreUncontrolledChangelistsEnabled(); }; /** * Returns a new changelist description if needed, appending validation tag. * * @param bInValidationResult The result of the validation step * @param InOriginalChangelistDescription Description of the changelist before modification * * @return The new changelist description */ FText UpdateChangelistDescriptionToSubmitIfNeeded(const bool bInValidationResult, const FText& InChangelistDescription) { auto GetChangelistValidationTag = [] { return LOCTEXT("ValidationTag", "#changelist validated"); }; auto ContainsValidationFlag = [&GetChangelistValidationTag](const FText& InChangelistDescription) { FString DescriptionString = InChangelistDescription.ToString(); FString ValidationString = GetChangelistValidationTag().ToString(); return DescriptionString.Find(ValidationString) != INDEX_NONE; }; auto RemoveValidationFlag = [&GetChangelistValidationTag](const FText& InChangelistDescription) { FString DescriptionString = InChangelistDescription.ToString(); FString ValidationString = GetChangelistValidationTag().ToString(); DescriptionString.ReplaceInline(*ValidationString, TEXT("")); return DescriptionString; }; if (bInValidationResult && USourceControlPreferences::IsValidationTagEnabled() && !ContainsValidationFlag(InChangelistDescription)) { FStringOutputDevice Str; Str.SetAutoEmitLineTerminator(true); Str.Log(InChangelistDescription); Str.Log(GetChangelistValidationTag()); return FText::FromString(Str); } if (!bInValidationResult && USourceControlPreferences::IsValidationTagEnabled() && ContainsValidationFlag(InChangelistDescription)) { FString NewChangelistDescription = RemoveValidationFlag(InChangelistDescription); return FText::FromString(NewChangelistDescription); } return InChangelistDescription; } } // Anonymous namespace DECLARE_DELEGATE(FOnSearchBoxExpanded) /** A button that expands a search box below itself when clicked. */ class SExpandableSearchButton : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SExpandableSearchButton) : _Style(&FAppStyle::Get().GetWidgetStyle("SearchBox")) {} /** Search box style (used to match the glass icon) */ SLATE_STYLE_ARGUMENT(FSearchBoxStyle, Style) /** Event fired when the associated search box is made visible */ SLATE_EVENT(FOnSearchBoxExpanded, OnSearchBoxExpanded) SLATE_END_ARGS() void Construct(const FArguments& InArgs, TSharedRef SearchBox) { OnSearchBoxExpanded = InArgs._OnSearchBoxExpanded; SearchStyle = InArgs._Style; SearchBox->SetVisibility(TAttribute::CreateSP(this, &SExpandableSearchButton::GetSearchBoxVisibility)); SearchBoxPtr = SearchBox; ChildSlot [ SNew(SCheckBox) .IsChecked(this, &SExpandableSearchButton::GetToggleButtonState) .OnCheckStateChanged(this, &SExpandableSearchButton::OnToggleButtonStateChanged) .Style(FAppStyle::Get(), "ToggleButtonCheckbox") .Padding(2.0f) .ToolTipText(NSLOCTEXT("ExpandableSearchArea", "ExpandCollapseSearchButton", "Expands or collapses the search text box")) [ SNew(SImage) .Image(&SearchStyle->GlassImage) .ColorAndOpacity(FSlateColor::UseForeground()) ] ]; } private: /** Sets whether or not the search area is expanded to expose the search box */ void OnToggleButtonStateChanged(ECheckBoxState CheckBoxState) { bIsExpanded = CheckBoxState == ECheckBoxState::Checked; if (TSharedPtr SearchBox = SearchBoxPtr.Pin()) { if (bIsExpanded) { OnSearchBoxExpanded.ExecuteIfBound(); // Focus the search box when it's shown FSlateApplication::Get().SetUserFocus(FSlateApplication::Get().GetUserIndexForKeyboard(), SearchBox, EFocusCause::SetDirectly); } else { // Clear the search box when it's hidden SearchBox->SetText(FText::GetEmpty()); } } } ECheckBoxState GetToggleButtonState() const { return bIsExpanded ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } EVisibility GetSearchBoxVisibility() const { return bIsExpanded ? EVisibility::Visible : EVisibility::Collapsed; } private: const FSearchBoxStyle* SearchStyle; FOnSearchBoxExpanded OnSearchBoxExpanded; TWeakPtr SearchBoxPtr; bool bIsExpanded = false; }; /** An expanded area to contain the changelists tree view or then uncontrolled changelists tree view. */ class SExpandableChangelistArea : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SExpandableChangelistArea) : _Style(&FAppStyle::Get().GetWidgetStyle("SearchBox")) , _HeaderText() , _ChangelistView() , _OnSearchBoxExpanded() , _OnNewChangelist() , _OnNewChangelistTooltip() , _NewButtonVisibility(EVisibility::Visible) , _SearchButtonVisibility(EVisibility::Visible) , _OnSearchTextChanged() {} /** Search box style (used to match the glass icon) */ SLATE_STYLE_ARGUMENT(FSearchBoxStyle, Style) /** Text displayed on the expandable area */ SLATE_ATTRIBUTE(FText, HeaderText) /** The tree element displayed as body. */ SLATE_ARGUMENT(TSharedPtr>, ChangelistView) /** Event fired when the associated search box is made visible */ SLATE_EVENT(FOnSearchBoxExpanded, OnSearchBoxExpanded) /** Event fired when the 'plus' button is clicked. */ SLATE_EVENT(FOnClicked, OnNewChangelist) /** Tooltip displayed over the 'plus' button. */ SLATE_ATTRIBUTE(FText, OnNewChangelistTooltip) /** Make the 'plus' button visible or not. */ SLATE_ARGUMENT(EVisibility, NewButtonVisibility) /** Make the 'search' button visible or not. */ SLATE_ARGUMENT(EVisibility, SearchButtonVisibility) /** Invoked whenever the searched text changes. */ SLATE_EVENT(FOnTextChanged, OnSearchTextChanged) SLATE_END_ARGS() void Construct(const FArguments& InArgs) { SearchBox = SNew(SSearchBox) .OnTextChanged(InArgs._OnSearchTextChanged); ChildSlot [ SAssignNew(ExpandableArea, SExpandableArea) .BorderImage(FAppStyle::Get().GetBrush("Brushes.Header")) .BodyBorderImage(FAppStyle::Get().GetBrush("Brushes.Recessed")) .HeaderPadding(FMargin(4.0f, 2.0f)) .AllowAnimatedTransition(false) .HeaderContent() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(InArgs._HeaderText) .TextStyle(FAppStyle::Get(), "ButtonText") .Font(FAppStyle::Get().GetFontStyle("NormalFontBold")) ] +SHorizontalBox::Slot() .VAlign(VAlign_Center) .HAlign(HAlign_Right) .AutoWidth() .Padding(4.0f, 0.0f, 0.0f, 0.0f) [ SNew(SButton) .ButtonStyle(FAppStyle::Get(), "SimpleButton") .ToolTipText(InArgs._OnNewChangelistTooltip) .OnClicked(InArgs._OnNewChangelist) .ContentPadding(FMargin(1, 0)) .Visibility(InArgs._NewButtonVisibility) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.PlusCircle")) .ColorAndOpacity(FSlateColor::UseForeground()) ] ] +SHorizontalBox::Slot() .VAlign(VAlign_Center) .HAlign(HAlign_Right) .AutoWidth() .Padding(4.0f, 0.0f) [ SNew(SExpandableSearchButton, SearchBox.ToSharedRef()) .Visibility(InArgs._SearchButtonVisibility) ] ] .BodyContent() [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() [ // Should blend in visually with the header but technically acts like part of the body SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("Brushes.Header")) .Padding(FMargin(4.0f, 2.0f)) [ SearchBox.ToSharedRef() ] ] + SVerticalBox::Slot() [ SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("Brushes.Recessed")) [ InArgs._ChangelistView.ToSharedRef() ] ] ] ]; } void SetExpanded(bool bExpanded) { ExpandableArea->SetExpanded(bExpanded); } bool IsExpanded() const { return ExpandableArea->IsExpanded(); } FText GetSearchedText() const { return SearchBox->GetText(); } TSharedPtr GetSearchBox() { return SearchBox; } private: TSharedPtr ExpandableArea; TSharedPtr SearchBox; }; void SSourceControlChangelistsWidget::FAsyncTimestampUpdater::DoWork() { for (FString& Pathname : RequestedFileTimestamps) { if (!bAbandon) { if (FPaths::IsRelative(Pathname)) { Pathname = FPaths::ConvertRelativePathToFull(Pathname); } QueriedFileTimestamps.Emplace(MoveTemp(Pathname), IFileManager::Get().GetTimeStamp(*Pathname)); } } } void SSourceControlChangelistsWidget::Construct(const FArguments& InArgs) { bShowingContentVersePath = FAssetToolsModule::GetModule().Get().ShowingContentVersePath(); // Register delegates ISourceControlModule& SCCModule = ISourceControlModule::Get(); FUncontrolledChangelistsModule& UncontrolledChangelistModule = FUncontrolledChangelistsModule::Get(); SCCModule.RegisterProviderChanged(FSourceControlProviderChanged::FDelegate::CreateSP(this, &SSourceControlChangelistsWidget::OnSourceControlProviderChanged)); SourceControlStateChangedDelegateHandle = SCCModule.GetProvider().RegisterSourceControlStateChanged_Handle(FSourceControlStateChanged::FDelegate::CreateSP(this, &SSourceControlChangelistsWidget::OnSourceControlStateChanged)); UncontrolledChangelistModule.OnUncontrolledChangelistModuleChanged.AddSP(this, &SSourceControlChangelistsWidget::OnUncontrolledChangelistStateChanged); // Monitor when packages are saved to update the timestamps. UPackage::PackageSavedWithContextEvent.AddSP(this, &SSourceControlChangelistsWidget::OnPackageSaved); IPluginManager::Get().OnPluginEdited().AddSP(this, &SSourceControlChangelistsWidget::OnPluginEdited); // No default sorting, sorting make the view slower, just pay the cost if user wants it. PrimarySortedColumn = FName(); UnsavedAssetsTreeNode.Add(MakeShared()); UnsavedAssetsTreeView = CreateChangelistTreeView(UnsavedAssetsTreeNode); ChangelistTreeView = CreateChangelistTreeView(ChangelistTreeNodes); UncontrolledChangelistTreeView = CreateChangelistTreeView(UncontrolledChangelistTreeNodes); FileListView = CreateChangelistFilesView(); UnsavedAssetsFileListView = CreateUnsavedAssetsFilesView(); FileListViewSwitcher = SNew(SWidgetSwitcher); FileListViewSwitcher->AddSlot().AttachWidget(FileListView.ToSharedRef()); FileListViewSwitcher->AddSlot().AttachWidget(UnsavedAssetsFileListView.ToSharedRef()); UnsavedAssetsExpandableArea = SNew(SExpandableChangelistArea) .HeaderText_Lambda([] { return FText::Format(LOCTEXT("SourceControl_UnsavedAssets", "Unsaved Assets ({0})"), FUnsavedAssetsTrackerModule::Get().GetUnsavedAssetNum()); }) .ChangelistView(UnsavedAssetsTreeView.ToSharedRef()) .NewButtonVisibility(EVisibility::Collapsed) .SearchButtonVisibility(EVisibility::Collapsed); ChangelistExpandableArea = SNew(SExpandableChangelistArea) .HeaderText_Lambda([this]() { return FText::Format(LOCTEXT("SourceControl_ChangeLists", "Changelists ({0})"), ChangelistTreeNodes.Num()); }) .ChangelistView(ChangelistTreeView.ToSharedRef()) .OnNewChangelist_Lambda([this](){ OnNewChangelist(); return FReply::Handled(); }) .OnNewChangelistTooltip(LOCTEXT("Create_New_Changelist", "Create a new changelist.")) .SearchButtonVisibility(EVisibility::Visible) .OnSearchTextChanged(this, &SSourceControlChangelistsWidget::OnChangelistSearchTextChanged); UncontrolledChangelistExpandableArea = SNew(SExpandableChangelistArea) .HeaderText_Lambda([this]() { return FText::Format(LOCTEXT("SourceControl_UncontrolledChangeLists", "Uncontrolled Changelists ({0})"), UncontrolledChangelistTreeNodes.Num()); }) .ChangelistView(UncontrolledChangelistTreeView.ToSharedRef()) .OnNewChangelist_Lambda([this]() { OnNewUncontrolledChangelist(); return FReply::Handled(); }) .OnNewChangelistTooltip(LOCTEXT("Create_New_Uncontrolled_Changelist", "Create a new uncontrolled changelist.")) .SearchButtonVisibility(EVisibility::Visible) // Functionality is planned but not fully implemented yet. .OnSearchTextChanged(this, &SSourceControlChangelistsWidget::OnUncontrolledChangelistSearchTextChanged); ChangelistTextFilter = MakeShared>(TTextFilter::FItemToStringArray::CreateSP(this, &SSourceControlChangelistsWidget::PopulateItemSearchStrings)); ChangelistTextFilter->OnChanged().AddSP(this, &SSourceControlChangelistsWidget::OnRefreshUI, ERefreshFlags::SourceControlChangelists); UncontrolledChangelistTextFilter = MakeShared>(TTextFilter::FItemToStringArray::CreateSP(this, &SSourceControlChangelistsWidget::PopulateItemSearchStrings)); UncontrolledChangelistTextFilter->OnChanged().AddSP(this, &SSourceControlChangelistsWidget::OnRefreshUI, ERefreshFlags::UncontrolledChangelists); FileTextFilter = MakeShared>(TTextFilter::FItemToStringArray::CreateSP(this, &SSourceControlChangelistsWidget::PopulateItemSearchStrings)); FileTextFilter->OnChanged().AddSP(this, &SSourceControlChangelistsWidget::OnRefreshUI, ERefreshFlags::All); FUnsavedAssetsTrackerModule::Get().OnUnsavedAssetAdded.AddSP(this, &SSourceControlChangelistsWidget::OnUnsavedAssetChanged); FUnsavedAssetsTrackerModule::Get().OnUnsavedAssetRemoved.AddSP(this, &SSourceControlChangelistsWidget::OnUnsavedAssetChanged); // Min/Max prevents making the Changelist Area too small and consequently prevent the file search box to go over the 'refresh' button. const float MinChangelistAreaRatio = 0.2f; const float MaxFileAreaRation = 1.0f - MinChangelistAreaRatio; ChildSlot [ SNew(SVerticalBox) +SVerticalBox::Slot() // For the toolbar (Refresh button) .AutoHeight() [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .Padding(4) [ SNew(SOverlay) // To align the file search box with the Changelist/File splitter, to compute ratio on the same width. +SOverlay::Slot() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .HAlign(HAlign_Left) .VAlign(VAlign_Center) .AutoWidth() [ MakeToolBar() ] ] +SOverlay::Slot() // That slots has the same width than the SSplitter and use the same left/right ratio to keep the search box over the file section. [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .FillWidth(TAttribute::CreateLambda([this, MinChangelistAreaRatio]() { return FMath::Max(MinChangelistAreaRatio, ChangelistAreaSize / (ChangelistAreaSize + FileAreaSize)); })) .VAlign(VAlign_Center) [ SNew(SSpacer) // This spacer uses the same width as the Changelist area on the left. They are kept in sync. ] +SHorizontalBox::Slot() .VAlign(VAlign_Center) .FillWidth(TAttribute::CreateLambda([this, MaxFileAreaRation]() { return FMath::Min(MaxFileAreaRation, FileAreaSize / (ChangelistAreaSize + FileAreaSize)); })) [ SAssignNew(FileSearchBox, SSearchBox) .HintText(LOCTEXT("Search_Files", "Search the files...")) .OnTextChanged(this, &SSourceControlChangelistsWidget::OnFileSearchTextChanged) ] ] ] ] +SVerticalBox::Slot() // Everything below the tools bar: changelist expandable areas + files views + status bar at the bottom [ SNew(SSplitter) .Orientation(EOrientation::Orient_Horizontal) .ResizeMode(ESplitterResizeMode::FixedPosition) // Left slot: Changelists and uncontrolled changelists areas +SSplitter::Slot() .Resizable(true) .SizeRule(SSplitter::FractionOfParent) .Value_Lambda([this, MinChangelistAreaRatio]() { return FMath::Max(MinChangelistAreaRatio, ChangelistAreaSize); }) .OnSlotResized_Lambda([this](float Size) { ChangelistAreaSize = Size; }) [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() [ SNew(SBox) .Visibility_Lambda([] { return AreUnsavedAssetsPresent() ? EVisibility::Visible : EVisibility::Collapsed; } ) [ UnsavedAssetsExpandableArea.ToSharedRef() ] ] +SVerticalBox::Slot() [ SNew(SOverlay) +SOverlay::Slot() [ SNew(SBox) .Visibility_Lambda([](){ return !AreChangelistsEnabled() ? EVisibility::Visible: EVisibility::Collapsed; }) .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("SourceControl_Disabled", "The revision control is disabled or it doesn't support changelists.")) ] ] +SOverlay::Slot() // Visible when both Controlled and Uncontrolled changelists are enabled (Need to split vertical space) [ SNew(SSplitter) .Orientation(EOrientation::Orient_Vertical) .Visibility_Lambda([]() { return AreControlledChangelistsEnabled() && AreUncontrolledChangelistsEnabled() ? EVisibility::Visible : EVisibility::Collapsed; }) // Top slot: Changelists +SSplitter::Slot() .SizeRule_Lambda([this](){ return ChangelistExpandableArea->IsExpanded() ? SSplitter::ESizeRule::FractionOfParent : SSplitter::ESizeRule::SizeToContent; }) .Value(0.7) [ ChangelistExpandableArea.ToSharedRef() ] // Bottom slot: Uncontrolled Changelists +SSplitter::Slot() .SizeRule_Lambda([this](){ return UncontrolledChangelistExpandableArea->IsExpanded() ? SSplitter::ESizeRule::FractionOfParent : SSplitter::ESizeRule::SizeToContent; }) .Value(0.3) [ UncontrolledChangelistExpandableArea.ToSharedRef() ] ] +SOverlay::Slot() // Visible when controlled changelists are enabled but not the uncontrolled ones. [ SNew(SBox) .Visibility_Lambda([](){ return AreControlledChangelistsEnabled() && !AreUncontrolledChangelistsEnabled() ? EVisibility::Visible : EVisibility::Collapsed; }) [ ChangelistExpandableArea.ToSharedRef() ] ] +SOverlay::Slot() // Visible when uncontrolled changelist are enabled, but not the controlled ones. [ SNew(SBox) .Visibility_Lambda([](){ return !AreControlledChangelistsEnabled() && AreUncontrolledChangelistsEnabled() ? EVisibility::Visible : EVisibility::Collapsed; }) [ UncontrolledChangelistExpandableArea.ToSharedRef() ] ] ] ] // Right slot: Files associated to the selected the changelist/uncontrolled changelist. +SSplitter::Slot() .Resizable(true) .SizeRule(SSplitter::FractionOfParent) .Value_Lambda([this, MaxFileAreaRation]() { return FMath::Min(MaxFileAreaRation, FileAreaSize); }) .OnSlotResized_Lambda([this](float Size) { FileAreaSize = Size; }) [ FileListViewSwitcher.ToSharedRef() ] ] +SVerticalBox::Slot() // Status bar (Always visible if uncontrolled changelist are enabled to keep the reconcile status visible at all time) .AutoHeight() [ SNew(SBox) .Padding(0, 3) .Visibility_Lambda([this](){ return FUncontrolledChangelistsModule::Get().IsEnabled() || !RefreshStatus.IsEmpty() ? EVisibility::Visible : EVisibility::Collapsed; }) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .HAlign(HAlign_Left) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text_Lambda([this]() { return RefreshStatus; }) ] +SHorizontalBox::Slot() .HAlign(HAlign_Right) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text_Lambda([]() { return FUncontrolledChangelistsModule::Get().GetReconcileStatus(); }) .Visibility_Lambda([]() { return AreUncontrolledChangelistsEnabled() ? EVisibility::Visible : EVisibility::Collapsed; }) ] ] ] ]; bShouldRefresh = true; } SSourceControlChangelistsWidget::~SSourceControlChangelistsWidget() { ISourceControlModule::Get().GetSourceControlFileStatusMonitor().StopMonitoringFiles(reinterpret_cast(this)); if (TimestampUpdateTask) { TimestampUpdateTask->TryAbandonTask(); TimestampUpdateTask->EnsureCompletion(/*bDoWorkOnThisThreadIfNotStarted*/false); TimestampUpdateTask.Reset(); } } TSharedRef SSourceControlChangelistsWidget::MakeToolBar() { FSlimHorizontalToolBarBuilder ToolBarBuilder(nullptr, FMultiBoxCustomization::None); ToolBarBuilder.AddToolBarButton( FUIAction( FExecuteAction::CreateLambda([this]() { RequestChangelistsRefresh(); })), NAME_None, LOCTEXT("SourceControl_RefreshButton", "Refresh"), LOCTEXT("SourceControl_RefreshButton_Tooltip", "Refreshes changelists from revision control provider."), FSlateIcon(FRevisionControlStyleManager::GetStyleSetName(), "RevisionControl.Actions.Refresh")); return ToolBarBuilder.MakeWidget(); } void SSourceControlChangelistsWidget::EditChangelistDescription(const FText& InNewChangelistDescription, const FSourceControlChangelistStatePtr& InChangelistState) { if (InChangelistState->SupportsPersistentDescription()) { TSharedRef EditChangelistOperation = ISourceControlOperation::Create(); EditChangelistOperation->SetDescription(InNewChangelistDescription); Execute(LOCTEXT("Updating_Changelist_Description", "Updating changelist description..."), EditChangelistOperation, InChangelistState->GetChangelist(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateLambda( [](const TSharedRef& Operation, ECommandResult::Type InResult) { if (InResult == ECommandResult::Succeeded) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Update_Changelist_Description_Succeeded", "Changelist description successfully updated."), SNotificationItem::CS_Success); } else if (InResult == ECommandResult::Failed) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Update_Changelist_Description_Failed", "Failed to update changelist description."), SNotificationItem::CS_Fail); } })); } else // Move everything to a new CL. Ex: the default P4 CL description cannot be saved. { TArray FilesToMove; Algo::Transform(InChangelistState->GetFilesStates(), FilesToMove, [](const TSharedRef& FileState) { return FileState->GetFilename(); }); TSharedRef NewChangelistOperation = ISourceControlOperation::Create(); NewChangelistOperation->SetDescription(InNewChangelistDescription); Execute(LOCTEXT("Saving_Into_New_Changelist", "Saving to a new changelist..."), NewChangelistOperation, FilesToMove, EConcurrency::Synchronous, FSourceControlOperationComplete::CreateLambda( [](const TSharedRef& Operation, ECommandResult::Type InResult) { if (InResult == ECommandResult::Succeeded) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Saving_New_Changelist_Succeeded", "New changelist saved"), SNotificationItem::CS_Success); } if (InResult == ECommandResult::Failed) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Saving_New_Changelist_Failed", "Failed to save to a new changelist."), SNotificationItem::CS_Fail); } })); } } void SSourceControlChangelistsWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { if (!ISourceControlModule::Get().IsEnabled() && !FUncontrolledChangelistsModule::Get().IsEnabled()) { return; } // Detect transitions of the source control being available/unavailable. Ex: When the user changes the source control in UI, the provider gets selected, // but it is not connected/available until the user accepts the settings. The source control doesn't have callback for availability and we want to refresh everything // once it gets available. if (ISourceControlModule::Get().IsEnabled() && !bSourceControlAvailable && ISourceControlModule::Get().GetProvider().IsAvailable()) { bSourceControlAvailable = true; bShouldRefresh = true; } bool bSortFileView = false; if (bShouldRefresh) { if (!bInitialRefreshDone) { // Ensure the UI is built from the current cached states once because all UI updates are hooked on 'state change' callbacks and those will only // be called when the state changed. OnRefreshUI(ERefreshFlags::All); bInitialRefreshDone = true; } RequestChangelistsRefresh(); bShouldRefresh = false; } if (bIsRefreshing) { TickRefreshStatus(InDeltaTime); } if (bUpdateMonitoredFileStatusList) { if (FileListNodes.IsEmpty()) { // Nothing to monitor. ISourceControlModule::Get().GetSourceControlFileStatusMonitor().StopMonitoringFiles(reinterpret_cast(this)); bUpdateMonitoredFileStatusList = false; } else if (IsFileViewSortedByFileStatusIcon()) { // If a changelist is selected, monitor all files status to honor the sort order on that column. if (ChangelistTreeView->GetNumItemsSelected() > 0) { RequestFileStatusRefresh(*ChangelistTreeView->GetSelectedItems()[0]); } else if (UncontrolledChangelistTreeView->GetNumItemsSelected() > 0) { RequestFileStatusRefresh(*UncontrolledChangelistTreeView->GetSelectedItems()[0]); } else if (UnsavedAssetsTreeView->GetNumItemsSelected() > 0) { RequestFileStatusRefresh(*UnsavedAssetsTreeView->GetSelectedItems()[0]); } bUpdateMonitoredFileStatusList = false; } else // Just monitor the files that are visible in the view. { // NOTE: In the tick where the view is updated/refreshed, the item count is usually zero, next tick it will have the correct value, so wait for it. int32 ItemCount = GetActiveFileListView().GetNumGeneratedChildren(); if (ItemCount > 0) { // Minimize the list of file status monitored to only include those in view. The ones outside of the visible area don't need // to be monitored unless the user sorts. int32 ItemStart = static_cast(GetActiveFileListView().GetScrollOffset()); // This give the offset in item count. int32 ItemEnd = ItemStart + ItemCount; TSet MonitoredFiles; for (int32 Index = ItemStart; Index < ItemEnd; ++Index) { MonitoredFiles.Emplace(StaticCastSharedPtr(FileListNodes[Index])->GetFullPathname()); } RequestFileStatusRefresh(MoveTemp(MonitoredFiles)); bUpdateMonitoredFileStatusList = false; } } } { const bool bNewShowingContentVersePath = FAssetToolsModule::GetModule().Get().ShowingContentVersePath(); if (bShowingContentVersePath != bNewShowingContentVersePath) { bShowingContentVersePath = bNewShowingContentVersePath; bSortFileView = true; } } if (bShouldRefreshVersePaths && bShowingContentVersePath) { bShouldRefreshVersePaths = false; for (const TSharedPtr& Item : FileListNodes) { // Resort of any of the Verse paths change. bSortFileView |= static_cast(Item.Get())->RefreshVersePath(); } } // If requested timestamps were gathered in background. if (TimestampUpdateTask && TimestampUpdateTask->IsDone()) { bool bUpdatedItemInView = false; // Start updating the offline files (if any) as we have a direct mapping between keys. if (OfflineFileItemCache.Num() > 0) { for (auto It = TimestampUpdateTask->GetTask().QueriedFileTimestamps.CreateIterator(); It; ++It) { if (TSharedPtr* FileViewItem = OfflineFileItemCache.Find(It->Key); FileViewItem && FileViewItem->IsValid()) { (*FileViewItem)->SetLastModifiedDateTime(It->Value); bUpdatedItemInView |= (*FileViewItem)->DisplayedUpdateNum == UpdateRequestNum; It.RemoveCurrent(); } } } // Update other files. There is no direct lookup key File -> FileItem, do a linear scan of the cache. int32 UpdateRemaining = TimestampUpdateTask->GetTask().QueriedFileTimestamps.Num(); auto UpdateCachedItems = [this, &UpdateRemaining, &bUpdatedItemInView](TMap, TSharedPtr>& Cache) { for (auto It = Cache.CreateIterator(); It && UpdateRemaining > 0; ++It) { if (It->Value->GetTreeItemType() == IChangelistTreeItem::File || It->Value->GetTreeItemType() == IChangelistTreeItem::ShelvedFile) { IFileViewTreeItem* FileViewItem = static_cast(It->Value.Get()); if (const FDateTime* Timestamp = TimestampUpdateTask->GetTask().QueriedFileTimestamps.Find(FileViewItem->GetFullPathname())) { FileViewItem->SetLastModifiedDateTime(*Timestamp); bUpdatedItemInView |= FileViewItem->DisplayedUpdateNum == UpdateRequestNum; --UpdateRemaining; } } } }; UpdateCachedItems(SourceControlItemCache); UpdateCachedItems(UncontrolledChangelistItemCache); TimestampUpdateTask.Reset(); if (bUpdatedItemInView && IsFileViewSortedByLastModifiedTimestamp()) { bSortFileView = true; } } if (bSortFileView) { SortFileView(); GetActiveFileListView().RequestListRefresh(); } if (!TimestampUpdateTask && OutdatedTimestampFiles.Num()) // Should be sufficient to run only one async task at the time. { // Query the timestamps asynchronously. TimestampUpdateTask = MakeUnique>(); TimestampUpdateTask->GetTask().RequestedFileTimestamps = MoveTemp(OutdatedTimestampFiles); TimestampUpdateTask->StartBackgroundTask(); } } void SSourceControlChangelistsWidget::RequestChangelistsRefresh() { bool bAnyProviderAvailable = false; if (AreControlledChangelistsEnabled()) { bAnyProviderAvailable = true; StartRefreshStatus(); TSharedRef UpdatePendingChangelistsOperation = ISourceControlOperation::Create(); UpdatePendingChangelistsOperation->SetUpdateAllChangelists(true); UpdatePendingChangelistsOperation->SetUpdateFilesStates(true); UpdatePendingChangelistsOperation->SetUpdateShelvedFilesStates(true); ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); SourceControlProvider.Execute(UpdatePendingChangelistsOperation, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SSourceControlChangelistsWidget::OnChangelistsStatusUpdated)); OnStartSourceControlOperation(UpdatePendingChangelistsOperation, LOCTEXT("SourceControl_UpdatingChangelist", "Updating changelists...")); } if (AreUncontrolledChangelistsEnabled()) { bAnyProviderAvailable = true; // This operation is synchronous and completes right away. FUncontrolledChangelistsModule::Get().UpdateStatus(); } if (!bAnyProviderAvailable) { // No provider available, clear changelist tree ClearChangelistsTree(); } } void SSourceControlChangelistsWidget::RequestFileStatusRefresh(const IChangelistTreeItem& Changelist) { TSet Pathnames; if (Changelist.GetTreeItemType() == IChangelistTreeItem::Changelist) { const FChangelistTreeItem& ChangelistItem = static_cast(Changelist); Algo::Transform(ChangelistItem.ChangelistState->GetFilesStates(), Pathnames, [](const TSharedRef& FileState) { return FileState->GetFilename(); }); } else if (Changelist.GetTreeItemType() == IChangelistTreeItem::ShelvedChangelist) { const FChangelistTreeItem& ChangelistItem = static_cast(*Changelist.GetParent().Get()); Algo::Transform(ChangelistItem.ChangelistState->GetShelvedFilesStates(), Pathnames, [](const TSharedRef& FileState) { return FileState->GetFilename(); }); } else if (Changelist.GetTreeItemType() == IChangelistTreeItem::UncontrolledChangelist) { const FUncontrolledChangelistTreeItem& ChangelistItem = static_cast(Changelist); Algo::Transform(ChangelistItem.UncontrolledChangelistState->GetFilenames(), Pathnames, [](const FString& Pathname) { return Pathname; }); } if (!Pathnames.IsEmpty()) { // Fire an async task to get the latest files status from source control to get the 'Checked Out By' status. RequestFileStatusRefresh(MoveTemp(Pathnames)); } } void SSourceControlChangelistsWidget::RequestFileStatusRefresh(TSet&& PathnamesToMonitor) { TRACE_CPUPROFILER_EVENT_SCOPE(SSourceControlChangelistsWidget::RequestFileStatusRefresh); if (!ISourceControlModule::Get().IsEnabled()) { return; } // NOTE: This is using the file status monitor and the status might be a bit old but not older than the // update period policy of the monitor. See FSourceControlFileStatusMonitor::SetUpdateStatusPeriodPolicy() // If the changelist contains files. if (!PathnamesToMonitor.IsEmpty()) { // Request an update only for the files that changed. ISourceControlModule::Get().GetSourceControlFileStatusMonitor().SetMonitoringFiles(reinterpret_cast(this), MoveTemp(PathnamesToMonitor), FSourceControlFileStatusMonitor::FOnSourceControlFileStatus()); // NOTE: The updated status are expected to come from OnSourceControlStateChanged() that is invoked when the source control internal state changes. } else { ISourceControlModule::Get().GetSourceControlFileStatusMonitor().StopMonitoringFiles(reinterpret_cast(this)); } } void SSourceControlChangelistsWidget::StartRefreshStatus() { bIsRefreshing = true; RefreshStatusStartSecs = FPlatformTime::Seconds(); } void SSourceControlChangelistsWidget::TickRefreshStatus(double InDeltaTime) { int32 RefreshStatusTimeElapsed = static_cast(FPlatformTime::Seconds() - RefreshStatusStartSecs); RefreshStatus = FText::Format(LOCTEXT("SourceControl_RefreshStatus", "Refreshing changelists... ({0} s)"), FText::AsNumber(RefreshStatusTimeElapsed)); } void SSourceControlChangelistsWidget::EndRefreshStatus() { bIsRefreshing = false; } void SSourceControlChangelistsWidget::ClearChangelistsTree() { if (!ChangelistTreeNodes.IsEmpty()) { ChangelistTreeNodes.Reset(); ChangelistTreeView->RequestTreeRefresh(); } if (!UncontrolledChangelistTreeNodes.IsEmpty()) { UncontrolledChangelistTreeNodes.Reset(); UncontrolledChangelistTreeView->RequestTreeRefresh(); } SourceControlItemCache.Reset(); UncontrolledChangelistItemCache.Reset(); OfflineFileItemCache.Reset(); } void SSourceControlChangelistsWidget::OnRefreshUnsavedAssetsWidgets(int64 CurrUpdateNum, const TFunction&)>& AddItemToFileView) { const bool bBeautifyPaths = true; const int64 PrevUpdateNum = CurrUpdateNum - 1; int32 DisplayedItemCount = 0; int32 VisitedItemCount = 0; TSharedPtr UnsavedAssetsItem = UnsavedAssetsTreeView->GetNumItemsSelected() > 0 ? UnsavedAssetsTreeView->GetSelectedItems()[0] : TSharedPtr(); if (!UnsavedAssetsItem) { return; } auto OnSourceControlCachedItemVisited = [CurrUpdateNum, &VisitedItemCount](IChangelistTreeItem* Item) { checkf(Item->VisitedUpdateNum < CurrUpdateNum, TEXT("The same revision control item was visited twice. It is likely present in more than one changelist")); Item->VisitedUpdateNum = CurrUpdateNum; ++VisitedItemCount; }; auto OnNewSourceControlItemCreated = [this, &OnSourceControlCachedItemVisited](FString Key, TSharedPtr Item) { OnSourceControlCachedItemVisited(Item.Get()); OfflineFileItemCache.Emplace(MoveTemp(Key), MoveTemp(Item)); }; auto AddItemToFileViewWrapper = [&AddItemToFileView, &DisplayedItemCount](TSharedPtr& Item) { ++DisplayedItemCount; AddItemToFileView(Item); }; auto RemoveDiscardedSourceControlItems = [this, VisitedItemCount, DisplayedItemCount, CurrUpdateNum]() { TRACE_CPUPROFILER_EVENT_SCOPE(SSourceControlChangelistsWidget::RemoveDiscardedSourceControlItems); check(OfflineFileItemCache.Num() >= VisitedItemCount); // Cannot visit items that are not cached. // Remove the items used to be visited but weren't visited during this iteration. int32 DiscardCount = OfflineFileItemCache.Num() - VisitedItemCount; for (auto It = OfflineFileItemCache.CreateIterator(); It && DiscardCount > 0 ; ++It) { if (It->Value->VisitedUpdateNum < CurrUpdateNum) { It.RemoveCurrent(); --DiscardCount; } } // Unless the FileTreeNode array was reset because the selected changelist changed, it still contains the union of items to display for this iteration and the item that were displayed last // iteration, so we need to remove item that aren't displayed anymore. (The FileTreeNode is not reset between updates to preserve the sort order as much as possible). if (FileListNodes.Num() > DisplayedItemCount) { // Remove items that were not displayed anymore. FileListNodes.RemoveAll([CurrUpdateNum](const TSharedPtr& Candidate) { return Candidate->DisplayedUpdateNum < CurrUpdateNum; }); } }; for (const FString& UnsavedAssetPath : FUnsavedAssetsTrackerModule::Get().GetUnsavedAssets()) { TSharedPtr FileItem; if (TSharedPtr* CachedFileItem = OfflineFileItemCache.Find(UnsavedAssetPath); CachedFileItem && CachedFileItem->IsValid()) { check((*CachedFileItem)->GetTreeItemType() == IChangelistTreeItem::OfflineFile); FileItem = StaticCastSharedPtr(*CachedFileItem); OnSourceControlCachedItemVisited(FileItem.Get()); } else { FileItem = MakeShared(UnsavedAssetPath); OnNewSourceControlItemCreated(UnsavedAssetPath, FileItem); } if (FileTextFilter->PassesFilter(*FileItem)) { UnsavedAssetsItem->AddChild(FileItem.ToSharedRef()); AddItemToFileViewWrapper(FileItem); } } RemoveDiscardedSourceControlItems(); UnsavedAssetsTreeView->RequestTreeRefresh(); } void SSourceControlChangelistsWidget::OnRefreshSourceControlWidgets(int64 CurrUpdateNum, const TFunction&)>& AddItemToFileView) { TRACE_CPUPROFILER_EVENT_SCOPE(SSourceControlChangelistsWidget::OnRefreshSourceControlWidgets); const bool bBeautifyPaths = true; const int64 PrevUpdateNum = CurrUpdateNum - 1; int32 DisplayedItemCount = 0; int32 VisitedItemCount = 0; TArray DuplicateItems; TSharedPtr SelectedChangelistItem = ChangelistTreeView->GetNumItemsSelected() > 0 ? ChangelistTreeView->GetSelectedItems()[0] : TSharedPtr(); auto HasSourceControlChangelistSelected = [&SelectedChangelistItem]() { return SelectedChangelistItem && (SelectedChangelistItem->GetTreeItemType() == IChangelistTreeItem::Changelist || SelectedChangelistItem->GetTreeItemType() == IChangelistTreeItem::ShelvedChangelist); }; auto OnSourceControlCachedItemVisited = [CurrUpdateNum, &VisitedItemCount, &DuplicateItems](IChangelistTreeItem* Item) { if (Item->VisitedUpdateNum < CurrUpdateNum) { Item->VisitedUpdateNum = CurrUpdateNum; ++VisitedItemCount; } else { // The same revision control item was visited twice. It is likely present in more than one changelist // This can happen in the case of a submit from outside of the editor ( e.g. from P4V ) // Handle it gracefully by ignoring the duplicate and then request a full changelists refresh for next tick DuplicateItems.Emplace(Item); } }; auto OnNewSourceControlItemCreated = [this, &OnSourceControlCachedItemVisited](TSharedPtr Key, TSharedPtr Item) { OnSourceControlCachedItemVisited(Item.Get()); SourceControlItemCache.Emplace(MoveTemp(Key), MoveTemp(Item)); }; auto AddItemToFileViewWrapper = [&AddItemToFileView, &DisplayedItemCount](TSharedPtr& Item) { ++DisplayedItemCount; AddItemToFileView(Item); }; auto RemoveDiscardedSourceControlItems = [this, VisitedItemCount, DisplayedItemCount, CurrUpdateNum, HasSourceControlChangelistSelected]() { TRACE_CPUPROFILER_EVENT_SCOPE(SSourceControlChangelistsWidget::RemoveDiscardedSourceControlItems); check(SourceControlItemCache.Num() >= VisitedItemCount); // Cannot visit items that are not cached. // Remove the items used to be visited but weren't visited during this iteration. int32 DiscardCount = SourceControlItemCache.Num() - VisitedItemCount; for (auto It = SourceControlItemCache.CreateIterator(); It && DiscardCount > 0 ; ++It) { if (It->Value->VisitedUpdateNum < CurrUpdateNum) { It.RemoveCurrent(); --DiscardCount; } } // If the source control feeds the file view. if (HasSourceControlChangelistSelected()) { // Unless the FileTreeNode array was reset because the selected changelist changed, it still contains the union of items to display for this iteration and the item that were displayed last // iteration, so we need to remove item that aren't displayed anymore. (The FileTreeNode is not reset between updates to preserve the sort order as much as possible). if (FileListNodes.Num() > DisplayedItemCount) { // Remove items that were not displayed anymore. FileListNodes.RemoveAll([CurrUpdateNum](const TSharedPtr& Candidate) { return Candidate->DisplayedUpdateNum < CurrUpdateNum; }); } } }; // Query the source control ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); TArray Changelists = SourceControlProvider.GetChangelists(EStateCacheUsage::Use); TArray ChangelistsStates; SourceControlProvider.GetState(Changelists, ChangelistsStates, EStateCacheUsage::Use); ChangelistTreeNodes.Reset(ChangelistsStates.Num()); for (const TSharedRef& ChangelistState : ChangelistsStates) { // Create or reuse the 'Changelists' and 'Shelved Files' node. TSharedPtr ChangelistTreeItem; TSharedPtr ShelvedChangelistTreeItem; if (TSharedPtr* CachedChangelistItem = SourceControlItemCache.Find(ChangelistState); CachedChangelistItem && CachedChangelistItem->IsValid()) { check((*CachedChangelistItem)->GetTreeItemType() == IChangelistTreeItem::Changelist); ChangelistTreeItem = StaticCastSharedPtr(*CachedChangelistItem); ChangelistTreeItem->RemoveAllChildren(); // Will relink the children against the active filter below. ChangelistTreeItem->ShelvedChangelistItem->RemoveAllChildren(); // Will relink the children against the active filter below. OnSourceControlCachedItemVisited(ChangelistTreeItem.Get()); } else { ChangelistTreeItem = MakeShared(ChangelistState); ChangelistTreeItem->ShelvedChangelistItem = MakeShared(); OnNewSourceControlItemCreated(ChangelistState, ChangelistTreeItem); } ShelvedChangelistTreeItem = ChangelistTreeItem->ShelvedChangelistItem; const bool bShelvedChangelistPassesFilter = ChangelistState->GetShelvedFilesStatesNum() > 0 && ChangelistTextFilter->PassesFilter(*ShelvedChangelistTreeItem); const bool bShelvedChangelistMustBeDisplayed = bShelvedChangelistPassesFilter && !ChangelistTextFilter->GetRawFilterText().IsEmpty(); const bool bChangelistPassedFilter = bShelvedChangelistMustBeDisplayed || ChangelistTextFilter->PassesFilter(*ChangelistTreeItem); if (bChangelistPassedFilter) { ChangelistTreeNodes.Add(ChangelistTreeItem); if (bShelvedChangelistPassesFilter) { ChangelistTreeItem->AddChild(ShelvedChangelistTreeItem.ToSharedRef()); if (bShelvedChangelistMustBeDisplayed) { ChangelistTreeView->SetItemExpansion(ChangelistTreeItem, /*bShouldExpand*/true); } } } // Create or reuse the 'File' nodes. const bool bChangelistSelected = SelectedChangelistItem == ChangelistTreeItem; for (const TSharedRef& FileState : ChangelistState->GetFilesStates()) { TSharedPtr FileItem; if (TSharedPtr* CachedFileItem = SourceControlItemCache.Find(FileState); CachedFileItem && CachedFileItem->IsValid()) { check((*CachedFileItem)->GetTreeItemType() == IChangelistTreeItem::File); FileItem = StaticCastSharedPtr(*CachedFileItem); OnSourceControlCachedItemVisited(FileItem.Get()); } else { FileItem = MakeShared(FileState, bBeautifyPaths); OnNewSourceControlItemCreated(FileState, FileItem); } if (bChangelistPassedFilter && FileTextFilter->PassesFilter(*FileItem) && !DuplicateItems.Contains(FileItem.Get())) { ChangelistTreeItem->AddChild(FileItem.ToSharedRef()); if (bChangelistSelected) { AddItemToFileViewWrapper(FileItem); } } } // Create or reuse the 'Shelved File' nodes. const bool bShelvedChangeslistSelected = SelectedChangelistItem == ShelvedChangelistTreeItem; for (const TSharedRef& ShelvedFileState : ChangelistState->GetShelvedFilesStates()) { TSharedPtr ShelvedFileItem; if (TSharedPtr* CachedShelvedFileItem = SourceControlItemCache.Find(ShelvedFileState); CachedShelvedFileItem && CachedShelvedFileItem->IsValid()) { check((*CachedShelvedFileItem)->GetTreeItemType() == IChangelistTreeItem::ShelvedFile); ShelvedFileItem = StaticCastSharedPtr(*CachedShelvedFileItem); OnSourceControlCachedItemVisited(ShelvedFileItem.Get()); } else { ShelvedFileItem = MakeShared(ShelvedFileState, bBeautifyPaths); OnNewSourceControlItemCreated(ShelvedFileState, ShelvedFileItem); } if (bChangelistPassedFilter && bShelvedChangelistPassesFilter && FileTextFilter->PassesFilter(*ShelvedFileItem) && !DuplicateItems.Contains(ShelvedFileItem.Get())) { ShelvedChangelistTreeItem->AddChild(ShelvedFileItem.ToSharedRef()); if (bShelvedChangeslistSelected) { AddItemToFileViewWrapper(ShelvedFileItem); } } } } RemoveDiscardedSourceControlItems(); ChangelistTreeView->RequestTreeRefresh(); // If a duplicate item was detected then request a changelists refresh in the next tick bShouldRefresh |= !DuplicateItems.IsEmpty(); } void SSourceControlChangelistsWidget::OnRefreshUncontrolledChangelistWidgets(int64 CurrUpdateNum, const TFunction&)>& AddItemToFileView) { TRACE_CPUPROFILER_EVENT_SCOPE(SSourceControlChangelistsWidget::OnRefreshUncontrolledChangelistWidgets); const bool bBeautifyPaths = true; const int64 PrevUpdateNum = CurrUpdateNum - 1; int32 DisplayedItemCount = 0; int32 VisitedItemCount = 0; TArray DuplicateItems; TSharedPtr SelectedChangelistItem = UncontrolledChangelistTreeView->GetNumItemsSelected() > 0 ? UncontrolledChangelistTreeView->GetSelectedItems()[0] : TSharedPtr(); auto HasUncontrolledChangelistSelected = [&SelectedChangelistItem]() { return SelectedChangelistItem && SelectedChangelistItem->GetTreeItemType() == IChangelistTreeItem::UncontrolledChangelist; }; auto OnUncontrolledChangelistCachedItemVisited = [CurrUpdateNum, &VisitedItemCount, &DuplicateItems](IChangelistTreeItem* Item) { if (Item->VisitedUpdateNum < CurrUpdateNum) { Item->VisitedUpdateNum = CurrUpdateNum; ++VisitedItemCount; } else { // The same uncontrolled item was visited twice. It is likely present in more than one uncontrolled changelist // Handle it gracefully by ignoring the duplicate and then request a full changelists refresh for next tick DuplicateItems.Emplace(Item); } }; auto OnNewUncontrolledChangelistItemCreated = [this, &OnUncontrolledChangelistCachedItemVisited](TSharedPtr Key, TSharedPtr Item) { OnUncontrolledChangelistCachedItemVisited(Item.Get()); UncontrolledChangelistItemCache.Emplace(MoveTemp(Key), MoveTemp(Item)); }; auto OnNewOfflineFileCreated = [this, &OnUncontrolledChangelistCachedItemVisited](FString Key, TSharedPtr Item) { OnUncontrolledChangelistCachedItemVisited(Item.Get()); OfflineFileItemCache.Emplace(MoveTemp(Key), MoveTemp(Item)); }; auto AddItemToFileViewWrapper = [&AddItemToFileView, &DisplayedItemCount](TSharedPtr& Item) { ++DisplayedItemCount; AddItemToFileView(Item); }; auto RemoveDiscardedUncontrolledChangelistItems = [this, VisitedItemCount, DisplayedItemCount, CurrUpdateNum, HasUncontrolledChangelistSelected]() { TRACE_CPUPROFILER_EVENT_SCOPE(SSourceControlChangelistsWidget::RemoveDiscardedUncontrolledChangelistItems); check(UncontrolledChangelistItemCache.Num() + OfflineFileItemCache.Num() >= VisitedItemCount); // Cannot visit items that are not cached. // Remove the items used to be visited but weren't visited during this iteration. int32 DiscardCount = UncontrolledChangelistItemCache.Num() + OfflineFileItemCache.Num() - VisitedItemCount; for (auto It = UncontrolledChangelistItemCache.CreateIterator(); It && DiscardCount > 0 ; ++It) { if (It->Value->VisitedUpdateNum < CurrUpdateNum) { It.RemoveCurrent(); --DiscardCount; } } for (auto It = OfflineFileItemCache.CreateIterator(); It && DiscardCount > 0 ; ++It) { if (It->Value->VisitedUpdateNum < CurrUpdateNum) { It.RemoveCurrent(); --DiscardCount; } } // If the uncontrolled changelist feeds the file view. if (HasUncontrolledChangelistSelected()) { // Unless the FileTreeNode array was reset because the selected changelist changed, it still contains the union of items to display for this iteration and the item that were displayed last // iteration, so we need to remove item that aren't displayed anymore. (The FileTreeNode is not reset between updates to preserve the sort order as much as possible). if (FileListNodes.Num() > DisplayedItemCount) { // Remove items that were not displayed anymore. FileListNodes.RemoveAll([CurrUpdateNum](const TSharedPtr& Candidate) { return Candidate->DisplayedUpdateNum < CurrUpdateNum; }); } } }; FUncontrolledChangelistsModule& UncontrolledChangelistModule = FUncontrolledChangelistsModule::Get(); TArray> UncontrolledChangelistStates = UncontrolledChangelistModule.GetChangelistStates(); UncontrolledChangelistTreeNodes.Reset(UncontrolledChangelistStates.Num()); for (const TSharedRef& UncontrolledChangelistState : UncontrolledChangelistStates) { // Create or reuse the 'Uncontrolled Changelist' node. TSharedPtr UncontrolledChangelistNode; if (TSharedPtr* CachedUncontrolledChangelistItem = UncontrolledChangelistItemCache.Find(UncontrolledChangelistState); CachedUncontrolledChangelistItem && CachedUncontrolledChangelistItem->IsValid()) { check((*CachedUncontrolledChangelistItem)->GetTreeItemType() == IChangelistTreeItem::UncontrolledChangelist); UncontrolledChangelistNode = StaticCastSharedPtr(*CachedUncontrolledChangelistItem); UncontrolledChangelistNode->RemoveAllChildren(); // Will relink the children against the active filter below. OnUncontrolledChangelistCachedItemVisited(UncontrolledChangelistNode.Get()); } else { UncontrolledChangelistNode = MakeShared(UncontrolledChangelistState); OnNewUncontrolledChangelistItemCreated(UncontrolledChangelistState, UncontrolledChangelistNode); } const bool bChangelistPassedFilter = UncontrolledChangelistTextFilter->PassesFilter(*UncontrolledChangelistNode); if (bChangelistPassedFilter) { UncontrolledChangelistTreeNodes.Add(UncontrolledChangelistNode); } // Create or reuse 'Uncontrolled' files. const bool bUncontrolledChangelistSelected = SelectedChangelistItem == UncontrolledChangelistNode; for (const TSharedRef& FileState : UncontrolledChangelistState->GetFilesStates()) { TSharedPtr UncontrolledFileItem; if (TSharedPtr* CachedUncontrolledFileItem = UncontrolledChangelistItemCache.Find(FileState); CachedUncontrolledFileItem && CachedUncontrolledFileItem->IsValid()) { check((*CachedUncontrolledFileItem)->GetTreeItemType() == IChangelistTreeItem::File); UncontrolledFileItem = StaticCastSharedPtr(*CachedUncontrolledFileItem); OnUncontrolledChangelistCachedItemVisited(UncontrolledFileItem.Get()); } else { UncontrolledFileItem = MakeShared(FileState, bBeautifyPaths); OnNewUncontrolledChangelistItemCreated(FileState, UncontrolledFileItem); } if (bChangelistPassedFilter && FileTextFilter->PassesFilter(*UncontrolledFileItem) && !DuplicateItems.Contains(UncontrolledFileItem.Get())) { UncontrolledChangelistNode->AddChild(UncontrolledFileItem.ToSharedRef()); if (bUncontrolledChangelistSelected) { AddItemToFileViewWrapper(UncontrolledFileItem); } } } // Create or reuse 'Offline' files. for (const FString& Filename : UncontrolledChangelistState->GetOfflineFiles()) { TSharedPtr OfflineFileItem; if (TSharedPtr* CachedOfflineFileItem = OfflineFileItemCache.Find(Filename)) { OfflineFileItem = *CachedOfflineFileItem; OnUncontrolledChangelistCachedItemVisited(OfflineFileItem.Get()); } else { OfflineFileItem = MakeShared(Filename); OnNewOfflineFileCreated(Filename, OfflineFileItem); } if (bChangelistPassedFilter && FileTextFilter->PassesFilter(*OfflineFileItem) && !DuplicateItems.Contains(OfflineFileItem.Get())) { UncontrolledChangelistNode->AddChild(OfflineFileItem.ToSharedRef()); if (bUncontrolledChangelistSelected) { AddItemToFileViewWrapper(OfflineFileItem); } } } } RemoveDiscardedUncontrolledChangelistItems(); UncontrolledChangelistTreeView->RequestTreeRefresh(); // If a duplicate item was detected then request a changelists refresh in the next tick bShouldRefresh |= !DuplicateItems.IsEmpty(); } void SSourceControlChangelistsWidget::OnRefreshUI(ERefreshFlags RefreshFlag) { TRACE_CPUPROFILER_EVENT_SCOPE(SSourceControlChangelistsWidget::OnRefreshUI); // The refresh algorithm tries to only update what changed. This is required to deal smoothly with reasonably large // changelists (5000 - 10000 files). The code focus on: // - Detecting added and removed items to do a 'delta' update (important for the file view) // - Preserving the sort order when possible. // - Caching and preserving file timestamps and update them using background task. // - Try performing expensive operations only on visible items. // This gives those benefits: // - The UI doesn't have to regenerate widget that didn't change. // - The selection of items that were displayed before and after an update is naturally preserved. // - The sort order is preserved most of the time. // - The cache keeps existing item even when filtered out, especially to avoiding to query the file timestamps (very expensive) too often. if (!AreChangelistsEnabled()) { ClearChangelistsTree(); return; } const int64 PrevUpdateNum = UpdateRequestNum; const int64 CurrUpdateNum = ++UpdateRequestNum; int32 NewDisplayedItemCount = 0; bool bDisplayedIconsPriorityChanged = false; auto AddItemToFileView = [this, &NewDisplayedItemCount, &bDisplayedIconsPriorityChanged, PrevUpdateNum, CurrUpdateNum](TSharedPtr& Item) { // If the items wasn't displayed last update. // Or, if FileListNodes was cleared from OnChangelistSelectionChanged if (Item->DisplayedUpdateNum != PrevUpdateNum || !FileListNodes.Contains(Item)) { checkfSlow(!FileListNodes.Contains(Item), TEXT("Inserting duplicated items. Something is wrong with the display update number.")) ++NewDisplayedItemCount; bUpdateMonitoredFileStatusList = true; // Add item at the end, will need to sort the list if the view is sorted. FileListNodes.Emplace(Item); // That's the first time the item is displayed. if (Item->DisplayedUpdateNum == -1) { Item->SetLastModifiedDateTime(IFileManager::Get().GetTimeStamp(*Item->GetFullPathname())); } } // Check when the file status changes (possibly invalidating sorting) int32 IconSortingPriority = Item->GetIconSortingPriority(); if (Item->DisplayedIconPriority != IconSortingPriority) { bDisplayedIconsPriorityChanged = true; Item->DisplayedIconPriority = IconSortingPriority; } // Mark this item as 'displayed' at this 'iteration'. Item->DisplayedUpdateNum = CurrUpdateNum; }; bool bFileViewUpdated = false; if (EnumHasAnyFlags(RefreshFlag, ERefreshFlags::SourceControlChangelists)) { OnRefreshSourceControlWidgets(CurrUpdateNum, AddItemToFileView); bFileViewUpdated |= (ChangelistTreeView->GetNumItemsSelected() > 0); } if (EnumHasAnyFlags(RefreshFlag, ERefreshFlags::UncontrolledChangelists)) { OnRefreshUncontrolledChangelistWidgets(CurrUpdateNum, AddItemToFileView); bFileViewUpdated |= (UncontrolledChangelistTreeView->GetNumItemsSelected() > 0); } if (EnumHasAnyFlags(RefreshFlag, ERefreshFlags::UnsavedAssets)) { OnRefreshUnsavedAssetsWidgets(CurrUpdateNum, AddItemToFileView); bFileViewUpdated |= (UnsavedAssetsTreeView->GetNumItemsSelected() > 0); } if (!bFileViewUpdated) { // The update didn't touch the file view, but the files (if any) were still displayed during this update interation, bump their displayed update number. for (TSharedPtr& Item : FileListNodes) { Item->DisplayedUpdateNum = CurrUpdateNum; } } if (FilesToSelect.Num() > 0) { TArray LocalFilesToSelect(MoveTemp(FilesToSelect)); SetSelectedFiles(LocalFilesToSelect); } const bool bNeedSorting = !PrimarySortedColumn.IsNone() && (NewDisplayedItemCount > 0 || (IsFileViewSortedByFileStatusIcon() && bDisplayedIconsPriorityChanged)); if (bNeedSorting) { SortFileView(); } GetActiveFileListView().RequestListRefresh(); } void SSourceControlChangelistsWidget::OnSourceControlProviderChanged(ISourceControlProvider& OldProvider, ISourceControlProvider& NewProvider) { OldProvider.UnregisterSourceControlStateChanged_Handle(SourceControlStateChangedDelegateHandle); SourceControlStateChangedDelegateHandle = NewProvider.RegisterSourceControlStateChanged_Handle(FSourceControlStateChanged::FDelegate::CreateSP(this, &SSourceControlChangelistsWidget::OnSourceControlStateChanged)); bSourceControlAvailable = NewProvider.IsAvailable(); // Check if it is connected. bShouldRefresh = true; if (&NewProvider != &OldProvider) { if (ChangelistTreeView->GetNumItemsSelected() > 0) { FileListNodes.Reset(); GetActiveFileListView().RequestListRefresh(); } ChangelistTreeNodes.Reset(); SourceControlItemCache.Reset(); ChangelistTreeView->RequestTreeRefresh(); } } void SSourceControlChangelistsWidget::OnSourceControlStateChanged() { TRACE_CPUPROFILER_EVENT_SCOPE(SSourceControlChangelistsWidget::OnSourceControlStateChanged); // NOTE: No need to call RequestChangelistsRefresh() to force the SCC to update internal states. We are being invoked because it was update, we just // need to update the UI to reflect those state changes. OnRefreshUI(ERefreshFlags::SourceControlChangelists); } void SSourceControlChangelistsWidget::OnChangelistsStatusUpdated(const TSharedRef& InOperation, ECommandResult::Type InType) { // NOTE: This is invoked when the 'FUpdatePendingChangelistsStatus' completes. No need to refresh the tree views because OnSourceControlStateChanged() is also called. OnEndSourceControlOperation(InOperation, InType); EndRefreshStatus(); } void SSourceControlChangelistsWidget::OnUncontrolledChangelistStateChanged() { OnRefreshUI(ERefreshFlags::UncontrolledChangelists); } FSourceControlChangelistStatePtr SSourceControlChangelistsWidget::GetCurrentChangelistState() { if (!ChangelistTreeView) { return nullptr; } TArray> SelectedItems = ChangelistTreeView->GetSelectedItems(); if (SelectedItems.Num() != 1 || SelectedItems[0]->GetTreeItemType() != IChangelistTreeItem::Changelist) { return nullptr; } return StaticCastSharedPtr(SelectedItems[0])->ChangelistState; } FUncontrolledChangelistStatePtr SSourceControlChangelistsWidget::GetCurrentUncontrolledChangelistState() const { if (!UncontrolledChangelistTreeView) { return nullptr; } TArray> SelectedItems = UncontrolledChangelistTreeView->GetSelectedItems(); if (SelectedItems.Num() != 1 || SelectedItems[0]->GetTreeItemType() != IChangelistTreeItem::UncontrolledChangelist) { return nullptr; } return StaticCastSharedPtr(SelectedItems[0])->UncontrolledChangelistState; } FSourceControlChangelistPtr SSourceControlChangelistsWidget::GetCurrentChangelist() { FSourceControlChangelistStatePtr ChangelistState = GetCurrentChangelistState(); return ChangelistState ? (FSourceControlChangelistPtr)(ChangelistState->GetChangelist()) : nullptr; } TOptional SSourceControlChangelistsWidget::GetCurrentUncontrolledChangelist() const { FUncontrolledChangelistStatePtr UncontrolledChangelistState = GetCurrentUncontrolledChangelistState(); return UncontrolledChangelistState ? UncontrolledChangelistState->Changelist : TOptional(); } FSourceControlChangelistStatePtr SSourceControlChangelistsWidget::GetChangelistStateFromSelection() { TArray SelectedItems = ChangelistTreeView->GetSelectedItems(); if (SelectedItems.Num() == 0) { return nullptr; } FChangelistTreeItemPtr Item = SelectedItems[0]; while (Item) { if (Item->GetTreeItemType() == IChangelistTreeItem::Changelist) { return StaticCastSharedPtr(Item)->ChangelistState; } Item = Item->GetParent(); } return nullptr; } FSourceControlChangelistPtr SSourceControlChangelistsWidget::GetChangelistFromSelection() { FSourceControlChangelistStatePtr ChangelistState = GetChangelistStateFromSelection(); return ChangelistState ? (FSourceControlChangelistPtr)(ChangelistState->GetChangelist()) : nullptr; } void SSourceControlChangelistsWidget::SetSelectedFiles(const TArray& Filenames) { if (bShouldRefresh || bIsRefreshing) { FilesToSelect = Filenames; return; } check(Filenames.Num() > 0); // Finds the Changelist tree item containing this Filename if it exists. auto FindChangelist = [this](const FString& Filename) -> TSharedPtr { for (const TSharedPtr& Item : ChangelistTreeNodes) { for (const TSharedPtr& ChildItem : Item->GetChildren()) { if (ChildItem->GetTreeItemType() == IChangelistTreeItem::File) { const FString& ChildFilename = StaticCastSharedPtr(ChildItem)->FileState->GetFilename(); if (ChildFilename.Compare(Filename, ESearchCase::IgnoreCase) == 0) { return Item; } } } } for (const TSharedPtr& Item : UncontrolledChangelistTreeNodes) { for (const TSharedPtr& ChildItem : Item->GetChildren()) { if (ChildItem->GetTreeItemType() == IChangelistTreeItem::File) { const FString& ChildFilename = StaticCastSharedPtr(ChildItem)->FileState->GetFilename(); if (ChildFilename.Compare(Filename, ESearchCase::IgnoreCase) == 0) { return Item; } } else if (ChildItem->GetTreeItemType() == IChangelistTreeItem::OfflineFile) { const FString& ChildFilename = StaticCastSharedPtr(ChildItem)->GetFilename(); if (ChildFilename.Compare(Filename, ESearchCase::IgnoreCase) == 0) { return Item; } } } } return nullptr; }; TSharedPtr FoundChangelistTreeItem = nullptr; // Find filename in Changelist, since filenames might not be in same Changelist, start from the last Filename as it might be the last selected one and give it priority for (int32 Index = Filenames.Num() - 1; Index >= 0; --Index) { if (TSharedPtr ChangelistTreeItem = FindChangelist(Filenames[Index])) { FoundChangelistTreeItem = ChangelistTreeItem; break; } } // Clear the current selection. GetActiveFileListView().ClearSelection(); TSharedPtr LastFoundItem; // If we found a Changelist, select files. if (FoundChangelistTreeItem) { // To make search faster store all filenames lower case TSet FilenamesLowerCase; Algo::Transform(Filenames, FilenamesLowerCase, [](const FString& Filename) { return Filename.ToLower(); }); for (const TSharedPtr& ChildItem : FoundChangelistTreeItem->GetChildren()) { if (ChildItem->GetTreeItemType() == IChangelistTreeItem::File) { const FString& ChildFilename = StaticCastSharedPtr(ChildItem)->FileState->GetFilename().ToLower(); if (FilenamesLowerCase.Contains(ChildFilename)) { GetActiveFileListView().SetItemSelection(ChildItem, /*bSelected*/true); LastFoundItem = ChildItem; } } else if (ChildItem->GetTreeItemType() == IChangelistTreeItem::OfflineFile) { const FString& ChildFilename = StaticCastSharedPtr(ChildItem)->GetFilename().ToLower(); if (FilenamesLowerCase.Contains(ChildFilename)) { GetActiveFileListView().SetItemSelection(ChildItem, /*bSelected*/true); LastFoundItem = ChildItem; } } } if (LastFoundItem) { if (FoundChangelistTreeItem->GetTreeItemType() == IChangelistTreeItem::Changelist) { // Ensure the area is expanded. ChangelistExpandableArea->SetExpanded(true); if (ChangelistTreeView->GetNumItemsSelected() == 0 || ChangelistTreeView->GetSelectedItems()[0] != FoundChangelistTreeItem) { ChangelistTreeView->SetSelection(FoundChangelistTreeItem); } } else if (FoundChangelistTreeItem->GetTreeItemType() == IChangelistTreeItem::UncontrolledChangelist) { // Ensure the area is expanded. UncontrolledChangelistExpandableArea->SetExpanded(true); if (UncontrolledChangelistTreeView->GetNumItemsSelected() == 0 || UncontrolledChangelistTreeView->GetSelectedItems()[0] != FoundChangelistTreeItem) { UncontrolledChangelistTreeView->SetSelection(FoundChangelistTreeItem); } } GetActiveFileListView().RequestScrollIntoView(LastFoundItem); } } } bool SSourceControlChangelistsWidget::HasFilesSelected() const { if (GetActiveFileListView().GetNumItemsSelected() == 0) { return false; } if (ChangelistTreeView->GetNumItemsSelected() > 0) { switch (ChangelistTreeView->GetSelectedItems()[0]->GetTreeItemType()) { case IChangelistTreeItem::Changelist: return true; // If this type of changeslist is selected and the file view as item selected, it means files are selected. case IChangelistTreeItem::UncontrolledChangelist: for (const TSharedPtr& Item : FileListView->GetSelectedItems()) { if (Item->GetTreeItemType() == IChangelistTreeItem::File) { return true; // Early out. } } default: break; } } if (UnsavedAssetsFileListView->GetSelectedItems().Num() > 0) { return true; } return false; } TArray SSourceControlChangelistsWidget::GetSelectedFiles() { TArray Files; for (const TSharedPtr& Item : FileListView->GetSelectedItems()) { if (Item->GetTreeItemType() == IChangelistTreeItem::File) { Files.Add(StaticCastSharedPtr(Item)->FileState->GetFilename()); } } return Files; } void SSourceControlChangelistsWidget::GetSelectedFiles(TArray& OutControlledFiles, TArray& OutUncontrolledFiles) { for (const TSharedPtr& Item : GetActiveFileListView().GetSelectedItems()) { if (Item->GetTreeItemType() == IChangelistTreeItem::File) { if (TSharedPtr Parent = Item->GetParent()) { const FString& Filename = StaticCastSharedPtr(Item)->FileState->GetFilename(); if (Parent->GetTreeItemType() == IChangelistTreeItem::Changelist) { OutControlledFiles.Add(Filename); } else if (Parent->GetTreeItemType() == IChangelistTreeItem::UncontrolledChangelist) { OutUncontrolledFiles.Add(Filename); } } } else if (Item->GetTreeItemType() == IChangelistTreeItem::OfflineFile) { if (TSharedPtr Parent = Item->GetParent()) { if (Parent->GetTreeItemType() == IChangelistTreeItem::UncontrolledChangelist) { const FString& Filename = StaticCastSharedPtr(Item)->GetFilename(); OutUncontrolledFiles.Add(Filename); } else if (Parent->GetTreeItemType() == IChangelistTreeItem::UnsavedAssets) { const FString& Filename = StaticCastSharedPtr(Item)->GetFilename(); OutControlledFiles.Add(Filename); } } } } } void SSourceControlChangelistsWidget::GetSelectedFileStates(TArray& OutControlledFileStates, TArray& OutUncontrolledFileStates) { TArray> SelectedItems = GetActiveFileListView().GetSelectedItems(); for (const TSharedPtr& Item : SelectedItems) { if (Item->GetTreeItemType() != IChangelistTreeItem::File) { continue; } if (const TSharedPtr& Parent = Item->GetParent()) { if (Parent->GetTreeItemType() == IChangelistTreeItem::Changelist) { OutControlledFileStates.Add(StaticCastSharedPtr(Item)->FileState); } else if (Parent->GetTreeItemType() == IChangelistTreeItem::UncontrolledChangelist) { OutUncontrolledFileStates.Add(StaticCastSharedPtr(Item)->FileState); } } } } bool SSourceControlChangelistsWidget::HasShelvedFilesSelected() const { return FileListView->GetNumItemsSelected() > 0 && ChangelistTreeView->GetNumItemsSelected() > 0 && ChangelistTreeView->GetSelectedItems()[0]->GetTreeItemType() == IChangelistTreeItem::ShelvedChangelist; } TArray SSourceControlChangelistsWidget::GetSelectedShelvedFiles() { TArray ShelvedFiles; for (const TSharedPtr& Item : FileListView->GetSelectedItems()) { if (Item->GetTreeItemType() == IChangelistTreeItem::ShelvedFile) { ShelvedFiles.Add(StaticCastSharedPtr(Item)->FileState->GetFilename()); } } // No individual 'shelved file' selected? if (ShelvedFiles.IsEmpty()) { // Check if the user selected the 'Shelved Files' changelist. for (const TSharedPtr& Item : ChangelistTreeView->GetSelectedItems()) { if (Item->GetTreeItemType() == IChangelistTreeItem::ShelvedChangelist) { // Add all items of the 'Shelved Files' changelist. for (const TSharedPtr& Children : Item->GetChildren()) { if (Children->GetTreeItemType() == IChangelistTreeItem::ShelvedFile) { ShelvedFiles.Add(StaticCastSharedPtr(Children)->FileState->GetFilename()); } } break; // UI only allows to select one changelist at the time. } } } return ShelvedFiles; } void SSourceControlChangelistsWidget::Execute(const FText& Message, const TSharedRef& InOperation, const EConcurrency::Type InConcurrency, const FSourceControlOperationComplete& InOperationCompleteDelegate) { return Execute(Message, InOperation, nullptr, TArray(), InConcurrency, InOperationCompleteDelegate); } void SSourceControlChangelistsWidget::Execute(const FText& Message, const TSharedRef& InOperation, TSharedPtr InChangelist, const EConcurrency::Type InConcurrency, const FSourceControlOperationComplete& InOperationCompleteDelegate) { return Execute(Message, InOperation, MoveTemp(InChangelist), TArray(), InConcurrency, InOperationCompleteDelegate); } void SSourceControlChangelistsWidget::Execute(const FText& Message, const TSharedRef& InOperation, const TArray& InFiles, EConcurrency::Type InConcurrency, const FSourceControlOperationComplete& InOperationCompleteDelegate) { return Execute(Message, InOperation, nullptr, InFiles, InConcurrency, InOperationCompleteDelegate); } void SSourceControlChangelistsWidget::Execute(const FText& Message, const TSharedRef& InOperation, TSharedPtr InChangelist, const TArray& InFiles, EConcurrency::Type InConcurrency, const FSourceControlOperationComplete& InOperationCompleteDelegate) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); // Start the operation. OnStartSourceControlOperation(InOperation, Message); if (InConcurrency == EConcurrency::Asynchronous) { // Pass a weak ptr to the lambda to protect in case the 'this' widget is closed/destroyed before the source control operation completes. TWeakPtr ThisWeak(StaticCastSharedRef(AsShared())); SourceControlProvider.Execute(InOperation, MoveTemp(InChangelist), InFiles, InConcurrency, FSourceControlOperationComplete::CreateLambda( [ThisWeak, InOperationCompleteDelegate](const TSharedRef& Operation, ECommandResult::Type InResult) { if (TSharedPtr ThisPtr = ThisWeak.Pin()) { InOperationCompleteDelegate.ExecuteIfBound(Operation, InResult); ThisPtr->OnEndSourceControlOperation(Operation, InResult); } })); } else { SSourceControlCommon::ExecuteChangelistOperationWithSlowTaskWrapper(Message, [&]() { ECommandResult::Type Result = SourceControlProvider.Execute(InOperation, InChangelist, InFiles, InConcurrency, InOperationCompleteDelegate); OnEndSourceControlOperation(InOperation, Result); }); } } void SSourceControlChangelistsWidget::ExecuteUncontrolledChangelistOperation(const FText& Message, const TFunction& UncontrolledOperation) { SSourceControlCommon::ExecuteUncontrolledChangelistOperationWithSlowTaskWrapper(Message, UncontrolledOperation); } void SSourceControlChangelistsWidget::OnStartSourceControlOperation(TSharedRef Operation, const FText& Message) { RefreshStatus = Message; // TODO: Should have a queue to stack async operations going on to correctly display concurrent async operations. } void SSourceControlChangelistsWidget::OnEndSourceControlOperation(const TSharedRef& Operation, ECommandResult::Type InType) { RefreshStatus = FText::GetEmpty(); // TODO: Should have a queue to stack async operations going on to correctly display concurrent async operations. } FReply SSourceControlChangelistsWidget::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { FText FailureMessage; if (InKeyEvent.GetKey() == EKeys::Enter) { // Submit the currently selected changelist (if any, and if conditions are met) if (CanSubmitChangelist(&FailureMessage)) { OnSubmitChangelist(); } else { FText Title(LOCTEXT("Cannot_Submit_Changelist_From_Key_Title", "Cannot Submit Changelist")); FMessageDialog::Open(EAppMsgType::Ok, EAppReturnType::Ok, FailureMessage, Title); } return FReply::Handled(); } else if (InKeyEvent.GetKey() == EKeys::Delete) { // Delete the currently selected changelist (if any, and if conditions are met) if (CanDeleteChangelist(&FailureMessage)) { OnDeleteChangelist(); } else { FText Title(LOCTEXT("Cannot_Delete_Changelist_From_Key_Title", "Cannot Delete Changelist")); FMessageDialog::Open(EAppMsgType::Ok, EAppReturnType::Ok, FailureMessage, Title); } return FReply::Handled(); } return FReply::Unhandled(); } void SSourceControlChangelistsWidget::OnNewChangelist() { FText ChangelistDescription; bool bOk = GetChangelistDescription( nullptr, LOCTEXT("SourceControl.Changelist.New.Title", "New Changelist..."), LOCTEXT("SourceControl.Changelist.New.Label", "Enter a description for the changelist:"), ChangelistDescription); if (!bOk) { return; } TSharedRef NewChangelistOperation = ISourceControlOperation::Create(); NewChangelistOperation->SetDescription(ChangelistDescription); Execute(LOCTEXT("Creating_Changelist", "Creating changelist..."), NewChangelistOperation, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateLambda( [](const TSharedRef& Operation, ECommandResult::Type InResult) { if (InResult == ECommandResult::Succeeded) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Create_Changelist_Succeeded", "Changelist successfully created."), SNotificationItem::CS_Success); } else if (InResult == ECommandResult::Failed) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Create_Changelist_Failed", "Failed to create the changelist."), SNotificationItem::CS_Fail); } })); } void SSourceControlChangelistsWidget::OnDeleteChangelist() { if (GetCurrentChangelist() == nullptr) { return; } TSharedRef DeleteChangelistOperation = ISourceControlOperation::Create(); Execute(LOCTEXT("Deleting_Changelist", "Deleting changelist..."), DeleteChangelistOperation, GetCurrentChangelist(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateLambda( [](const TSharedRef& Operation, ECommandResult::Type InResult) { if (InResult == ECommandResult::Succeeded) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Delete_Changelist_Succeeded", "Changelist successfully deleted."), SNotificationItem::CS_Success); } else if (InResult == ECommandResult::Failed) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Delete_Changelist_Failed", "Failed to delete the selected changelist."), SNotificationItem::CS_Fail); } })); } bool SSourceControlChangelistsWidget::CanDeleteChangelist() { return CanDeleteChangelist(/*OutFailureMessage*/nullptr); } bool SSourceControlChangelistsWidget::CanDeleteChangelist(FText* OutFailureMessage) { FSourceControlChangelistStatePtr ChangelistState = GetCurrentChangelistState(); if (ChangelistState == nullptr) { if (OutFailureMessage) { *OutFailureMessage = LOCTEXT("Cannot_Delete_No_Changelist", "No changelist selected."); } return false; } else if (!ChangelistState->GetChangelist()->CanDelete()) // Check if this changelist is deletable (ex. P4 default one is not deletable). { if (OutFailureMessage) { *OutFailureMessage = LOCTEXT("Cannot_Delete_Changelist_Not_Deletable", "The selected changelist cannot be deleted."); } return false; } else if (ChangelistState->GetFilesStatesNum() > 0 || ChangelistState->GetShelvedFilesStatesNum() > 0) { if (OutFailureMessage) { *OutFailureMessage = LOCTEXT("Cannot_Delete_Changelist_Not_Empty", "The changelist is not empty."); } return false; } return true; } void SSourceControlChangelistsWidget::OnEditChangelist() { FSourceControlChangelistStatePtr ChangelistState = GetCurrentChangelistState(); if(ChangelistState == nullptr) { return; } FText NewChangelistDescription = ChangelistState->GetDescriptionText(); bool bOk = GetChangelistDescription( nullptr, LOCTEXT("SourceControl.Changelist.New.Title2", "Edit Changelist..."), LOCTEXT("SourceControl.Changelist.New.Label2", "Enter a new description for the changelist:"), NewChangelistDescription); if (!bOk) { return; } EditChangelistDescription(NewChangelistDescription, ChangelistState); } void SSourceControlChangelistsWidget::OnRevertUnchanged() { TSharedRef RevertUnchangedOperation = ISourceControlOperation::Create(); Execute(LOCTEXT("Reverting_Unchanged_Files", "Reverting unchanged file(s)..."), RevertUnchangedOperation, GetChangelistFromSelection(), GetSelectedFiles(), EConcurrency::Synchronous, FSourceControlOperationComplete::CreateLambda( [](const TSharedRef& Operation, ECommandResult::Type InType) { // NOTE: This operation message should tell how many files were reverted and how many weren't. if (Operation->GetResultInfo().ErrorMessages.Num() == 0) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Revert_Unchanged_Files_Succeeded", "Unchanged files were reverted."), SNotificationItem::CS_Success); } else if (InType == ECommandResult::Failed) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Revert_Unchanged_Files_Failed", "Failed to revert unchanged files."), SNotificationItem::CS_Fail); } })); } bool SSourceControlChangelistsWidget::CanRevertUnchanged() { return HasFilesSelected() || (GetCurrentChangelistState() && GetCurrentChangelistState()->GetFilesStatesNum() > 0); } void SSourceControlChangelistsWidget::OnRevert() { FText DialogText; FText DialogTitle; TArray SelectedControlledFiles; TArray SelectedUncontrolledFiles; GetSelectedFiles(SelectedControlledFiles, SelectedUncontrolledFiles); // Apply to the entire changelist only of there are no files selected. const bool bApplyOnChangelist = (SelectedControlledFiles.Num() == 0 && SelectedUncontrolledFiles.Num() == 0); if (bApplyOnChangelist) { DialogText = LOCTEXT("SourceControl_ConfirmRevertChangelist", "Are you sure you want to revert this changelist?"); DialogTitle = LOCTEXT("SourceControl_ConfirmRevertChangelist_Title", "Confirm changelist revert"); } else { DialogText = LOCTEXT("SourceControl_ConfirmRevertFiles", "Are you sure you want to revert the selected files?"); DialogTitle = LOCTEXT("SourceControl_ConfirmReverFiles_Title", "Confirm files revert"); } EAppReturnType::Type UserConfirmation = FMessageDialog::Open(EAppMsgType::OkCancel, EAppReturnType::Ok, DialogText, DialogTitle); if (UserConfirmation != EAppReturnType::Ok) { return; } // Can only have one changelist selected at the time in the left split view (either a 'Changelist' or a 'Uncontrolled Changelist') if (TSharedPtr SelectedChangelist = GetChangelistFromSelection()) { // No specific files selected, pick all the files in the selected the changelist. if (SelectedControlledFiles.IsEmpty()) { // Find all the files in that changelist. if (FSourceControlChangelistStatePtr ChangelistState = ISourceControlModule::Get().GetProvider().GetState(SelectedChangelist.ToSharedRef(), EStateCacheUsage::Use)) { Algo::Transform(ChangelistState->GetFilesStates(), SelectedControlledFiles, [](const FSourceControlStateRef& FileState) { return FileState->GetFilename(); }); } } if (!SelectedControlledFiles.IsEmpty()) { TArray PackageFiles; TArray NonPackageFiles; for (const FString& Filename : SelectedControlledFiles) { if (FPackageName::IsPackageFilename(Filename)) { PackageFiles.Emplace(Filename); } else { NonPackageFiles.Emplace(Filename); } } SSourceControlCommon::ExecuteChangelistOperationWithSlowTaskWrapper(LOCTEXT("Reverting_Files", "Reverting file(s)..."), [&PackageFiles, &NonPackageFiles]() { if ((PackageFiles.Num() == 0 || SourceControlHelpers::RevertAndReloadPackages(PackageFiles)) && (NonPackageFiles.Num() == 0 || SourceControlHelpers::RevertFiles(NonPackageFiles))) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Revert_Files_Succeeded", "The selected file(s) were reverted."), SNotificationItem::CS_Success); } else { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Revert_Files_Failed", "Failed to revert the selected file(s)."), SNotificationItem::CS_Fail); } }); } } else if (TSharedPtr SelectedUncontrolledChangelist = GetCurrentUncontrolledChangelistState()) { // No individual uncontrolled files were selected, revert all the files from the selected uncontrolled changelist. if (SelectedUncontrolledFiles.IsEmpty()) { Algo::Transform(SelectedUncontrolledChangelist->GetFilesStates(), SelectedUncontrolledFiles, [](const FSourceControlStateRef& State) { return State->GetFilename(); }); } // Revert uncontrolled files (if any). if (!SelectedUncontrolledFiles.IsEmpty()) { ExecuteUncontrolledChangelistOperation(LOCTEXT("Reverting_Uncontrolled_Files", "Reverting uncontrolled files..."), [&SelectedUncontrolledFiles]() { if (FUncontrolledChangelistsModule::Get().OnRevert(SelectedUncontrolledFiles)) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Revert_Uncontrolled_Files_Succeeded", "The selected uncontrolled file(s) were reverted."), SNotificationItem::CS_Success); } else { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Revert_Uncontrolled_Files_Failed", "Failed to revert the selected uncontrolled file(s)."), SNotificationItem::CS_Fail); } }); } } // No changelist selected (and consequently, no files displayed that could be selected). } bool SSourceControlChangelistsWidget::CanRevert() { FSourceControlChangelistStatePtr CurrentChangelistState = GetCurrentChangelistState(); FUncontrolledChangelistStatePtr CurrentUncontrolledChangelistState = GetCurrentUncontrolledChangelistState(); return HasFilesSelected() || (CurrentChangelistState.IsValid() && CurrentChangelistState->GetFilesStatesNum() > 0) || (CurrentUncontrolledChangelistState.IsValid() && CurrentUncontrolledChangelistState->GetFilesStates().Num() > 0); } void SSourceControlChangelistsWidget::OnShelve() { FSourceControlChangelistStatePtr CurrentChangelist = GetChangelistStateFromSelection(); if (!CurrentChangelist) { return; } FText ChangelistDescription = CurrentChangelist->GetDescriptionText(); if (ChangelistDescription.IsEmptyOrWhitespace()) { bool bOk = GetChangelistDescription( nullptr, LOCTEXT("SourceControl.Changelist.NewShelve", "Shelving files..."), LOCTEXT("SourceControl.Changelist.NewShelve.Label", "Enter a description for the changelist holding the shelve:"), ChangelistDescription); if (!bOk) { // User cancelled entering a changelist description; abort shelve return; } } TSharedRef ShelveOperation = ISourceControlOperation::Create(); ShelveOperation->SetDescription(ChangelistDescription); Execute(LOCTEXT("Shelving_Files", "Shelving file(s)..."), ShelveOperation, CurrentChangelist->GetChangelist(), GetSelectedFiles(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateLambda( [](const TSharedRef& Operation, ECommandResult::Type InResult) { if (InResult == ECommandResult::Succeeded) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Shelve_Files_Succeeded", "The selected file(s) were shelved."), SNotificationItem::CS_Success); } else if (InResult == ECommandResult::Failed) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Shelve_Files_Failed", "Failed to shelved the selected file(s)."), SNotificationItem::CS_Fail); } })); } void SSourceControlChangelistsWidget::OnUnshelve() { TSharedRef UnshelveOperation = ISourceControlOperation::Create(); Execute(LOCTEXT("Unshelving_Files", "Unshelving file(s)..."), UnshelveOperation, GetChangelistFromSelection(), GetSelectedShelvedFiles(), EConcurrency::Synchronous, FSourceControlOperationComplete::CreateLambda( [](const TSharedRef& Operation, ECommandResult::Type InResult) { if (InResult == ECommandResult::Succeeded) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Unshelve_Files_Succeeded", "The selected file(s) were unshelved."), SNotificationItem::CS_Success); } else if (InResult == ECommandResult::Failed) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Unshelve_Files_Failed", "Failed to unshelved the selected file(s)."), SNotificationItem::CS_Fail); } })); } void SSourceControlChangelistsWidget::OnDeleteShelvedFiles() { TSharedRef DeleteShelvedOperation = ISourceControlOperation::Create(); Execute(LOCTEXT("Deleting_Shelved_Files", "Deleting shelved file(s)..."), DeleteShelvedOperation, GetChangelistFromSelection(), GetSelectedShelvedFiles(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateLambda( [](const TSharedRef& Operation, ECommandResult::Type InResult) { if (InResult == ECommandResult::Succeeded) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Delete_Shelved_Files_Succeeded", "The selected shelved file(s) were deleted."), SNotificationItem::CS_Success); } else if (InResult == ECommandResult::Failed) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Delete_Shelved_Files_Failed", "Failed to delete the selected shelved file(s)."), SNotificationItem::CS_Fail); } })); } static bool GetChangelistValidationResult(FSourceControlChangelistPtr InChangelist, FString& OutValidationTitleText, FString& OutValidationWarningsText, FString& OutValidationErrorsText) { FSourceControlPreSubmitDataValidationDelegate ValidationDelegate = ISourceControlModule::Get().GetRegisteredPreSubmitDataValidation(); EDataValidationResult ValidationResult = EDataValidationResult::NotValidated; TArray ValidationErrors; TArray ValidationWarnings; bool bValidationResult = true; if (ValidationDelegate.ExecuteIfBound(InChangelist, ValidationResult, ValidationErrors, ValidationWarnings)) { EMessageSeverity::Type MessageSeverity = EMessageSeverity::Info; if (ValidationResult == EDataValidationResult::Invalid || ValidationErrors.Num() > 0) { OutValidationTitleText = LOCTEXT("SourceControl.Submit.ChangelistValidationError", "Changelist validation failed!").ToString(); bValidationResult = false; MessageSeverity = EMessageSeverity::Error; } else if (ValidationResult == EDataValidationResult::NotValidated || ValidationWarnings.Num() > 0) { OutValidationTitleText = LOCTEXT("SourceControl.Submit.ChangelistValidationWarning", "Changelist validation has warnings!").ToString(); MessageSeverity = EMessageSeverity::Warning; } else { OutValidationTitleText = LOCTEXT("SourceControl.Submit.ChangelistValidationSuccess", "Changelist validation successful!").ToString(); } auto AppendInfo = [](const TArray& Info, const FString& InfoType, FString& OutText) { const int32 MaxNumLinesDisplayed = 5; int32 NumLinesDisplayed = 0; if (Info.Num() > 0) { OutText += LINE_TERMINATOR; OutText += FString::Printf(TEXT("Encountered %d %s:"), Info.Num(), *InfoType); for (const FText& Line : Info) { if (NumLinesDisplayed >= MaxNumLinesDisplayed) { OutText += LINE_TERMINATOR; OutText += FString::Printf(TEXT("See log for complete list of %s"), *InfoType); break; } OutText += LINE_TERMINATOR; OutText += Line.ToString(); ++NumLinesDisplayed; } } }; AppendInfo(ValidationErrors, TEXT("errors"), OutValidationErrorsText); AppendInfo(ValidationWarnings, TEXT("warnings"), OutValidationWarningsText); } return bValidationResult; } static bool GetOnPresubmitResult(FSourceControlChangelistStatePtr Changelist, FChangeListDescription& Description) { const TArray& FileStates = Changelist->GetFilesStates(); FText FailureMsg; if (!TryToVirtualizeFilesToSubmit(FileStates, Description.Description, FailureMsg)) { // Setup the notification for operation feedback FNotificationInfo Info(FailureMsg); Info.Text = LOCTEXT("SCC_Checkin_Failed", "Failed to check in files!"); Info.ExpireDuration = 8.0f; Info.HyperlinkText = LOCTEXT("SCC_Checkin_ShowLog", "Show Message Log"); Info.Hyperlink = FSimpleDelegate::CreateLambda([]() { FMessageLog("SourceControl").Open(EMessageSeverity::Error, true); }); TSharedPtr Notification = FSlateNotificationManager::Get().AddNotification(Info); Notification->SetCompletionState(SNotificationItem::CS_Fail); return false; } return true; } void SSourceControlChangelistsWidget::OnSubmitChangelist() { FSourceControlChangelistStatePtr ChangelistState = GetCurrentChangelistState(); if (!ChangelistState) { return; } // first check if there is a submit override bound if (ISourceControlWindowsModule::Get().SubmitOverrideDelegate.IsBound()) { // save the changelist description from the widget to perforce const FString Identifier = ChangelistState->GetChangelist()->GetIdentifier(); SSubmitOverrideParameters SubmitOverrideParameters; SubmitOverrideParameters.Description = ChangelistState->GetDescriptionText().ToString(); SubmitOverrideParameters.ToSubmit.SetSubtype(Identifier); FSubmitOverrideReply SubmitOverrideReply = ISourceControlWindowsModule::Get().SubmitOverrideDelegate.Execute(SubmitOverrideParameters); switch (SubmitOverrideReply) { ////////////////////////////////////////////////////////// case FSubmitOverrideReply::Handled: { FNotificationInfo Info(LOCTEXT("SCC_Checkin_SubmitOverride_Succeeded", "Successfully invoked the submit override!")); Info.Text = LOCTEXT("SCC_Checkin_SubmitOverride_Succeeded", "Successfully invoked the submit override!"); Info.ExpireDuration = 8.0f; Info.HyperlinkText = LOCTEXT("SCC_Checkin_ShowLog", "Show Message Log"); Info.Hyperlink = FSimpleDelegate::CreateLambda([]() { FMessageLog("SourceControl").Open(EMessageSeverity::Warning, true); }); TSharedPtr Notification = FSlateNotificationManager::Get().AddNotification(Info); Notification->SetCompletionState(SNotificationItem::CS_Success); this->OnRefreshUI(ERefreshFlags::SourceControlChangelists); return; } ////////////////////////////////////////////////////////// case FSubmitOverrideReply::Error: { FNotificationInfo Info(LOCTEXT("SCC_Checkin_SubmitOverride_Failed", "Failed to invoke the submit override!")); Info.Text = LOCTEXT("SCC_Checkin_SubmitOverride_Failed", "Failed to invoke the submit override!"); Info.ExpireDuration = 8.0f; Info.HyperlinkText = LOCTEXT("SCC_Checkin_ShowLog", "Show Message Log"); Info.Hyperlink = FSimpleDelegate::CreateLambda([]() { FMessageLog("SourceControl").Open(EMessageSeverity::Error, true); }); TSharedPtr Notification = FSlateNotificationManager::Get().AddNotification(Info); Notification->SetCompletionState(SNotificationItem::CS_Fail); this->OnRefreshUI(ERefreshFlags::SourceControlChangelists); return; } ////////////////////////////////////////////////////////// case FSubmitOverrideReply::ProviderNotSupported: default: // continue default flow break; } } FString ChangelistValidationTitle; FString ChangelistValidationWarningsText; FString ChangelistValidationErrorsText; bool bValidationResult = GetChangelistValidationResult(ChangelistState->GetChangelist(), ChangelistValidationTitle, ChangelistValidationWarningsText, ChangelistValidationErrorsText); // The description from the source control. const FText CurrentChangelistDescription = ChangelistState->GetDescriptionText(); const bool bAskForChangelistDescription = (CurrentChangelistDescription.IsEmptyOrWhitespace()); // The description possibly updated with the #validated proposed to the user. FText ChangelistDescriptionToSubmit = UpdateChangelistDescriptionToSubmitIfNeeded(bValidationResult, CurrentChangelistDescription); // The description once edited by the user in the Submit window. FText UserEditChangelistDescription = ChangelistDescriptionToSubmit; TSharedRef NewWindow = SNew(SWindow) .Title(NSLOCTEXT("SourceControl.ConfirmSubmit", "Title", "Confirm changelist submit")) .SizingRule(ESizingRule::UserSized) .ClientSize(FVector2D(600, 400)) .SupportsMaximize(true) .SupportsMinimize(false); TSharedRef SourceControlWidget = SNew(SSourceControlSubmitWidget) .ParentWindow(NewWindow) .Items(ChangelistState->GetFilesStates()) .Description(ChangelistDescriptionToSubmit) .ChangeValidationResult(ChangelistValidationTitle) .ChangeValidationWarnings(ChangelistValidationWarningsText) .ChangeValidationErrors(ChangelistValidationErrorsText) .AllowDescriptionChange(true) .AllowUncheckFiles(false) .AllowKeepCheckedOut(true) .AllowSubmit(bValidationResult) .AllowSaveAndClose(true); NewWindow->SetContent( SourceControlWidget ); FSlateApplication::Get().AddModalWindow(NewWindow, NULL); bool bSaveDescriptionOnSubmitFailure = false; bool bCheckinSuccess = false; if (SourceControlWidget->GetResult() == ESubmitResults::SUBMIT_ACCEPTED) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); FChangeListDescription Description; TSharedRef SubmitChangelistOperation = ISourceControlOperation::Create(); SubmitChangelistOperation->SetKeepCheckedOut(SourceControlWidget->WantToKeepCheckedOut()); // Get the changelist description the user had when he hit the 'submit' button. SourceControlWidget->FillChangeListDescription(Description); UserEditChangelistDescription = Description.Description; // Check if any of the presubmit hooks fail. (This might also update the changelist description) if (GetOnPresubmitResult(ChangelistState, Description)) { // If the user modified the description, ensure the 'validation tag' wasn't removed if validation is enabled. if (!ChangelistDescriptionToSubmit.EqualTo(Description.Description)) { SubmitChangelistOperation->SetDescription(UpdateChangelistDescriptionToSubmitIfNeeded(bValidationResult, Description.Description)); } else { SubmitChangelistOperation->SetDescription(Description.Description); } Execute(LOCTEXT("Submitting_Changelist", "Submitting changelist..."), SubmitChangelistOperation, ChangelistState->GetChangelist(), EConcurrency::Synchronous, FSourceControlOperationComplete::CreateLambda( [&SubmitChangelistOperation, &bCheckinSuccess](const TSharedRef& Operation, ECommandResult::Type InResult) { if (InResult == ECommandResult::Succeeded) { TOptional NotificationInfo = SSourceControlCommon::ConstructSourceControlOperationNotification(SubmitChangelistOperation->GetSuccessMessage()); if (NotificationInfo) { NotificationInfo->bUseCopyToClipboard = true; SSourceControlCommon::DisplaySourceControlOperationNotification(NotificationInfo.GetValue(), SNotificationItem::CS_Success); } bCheckinSuccess = true; } else if (InResult == ECommandResult::Failed) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("SCC_Checkin_Failed", "Failed to check in files!"), SNotificationItem::CS_Fail); } })); } // If something went wrong with the submit, try to preserve the changelist edited by the user (if he edited). bSaveDescriptionOnSubmitFailure = (!bCheckinSuccess && !UserEditChangelistDescription.EqualTo(ChangelistDescriptionToSubmit)); } if (SourceControlWidget->GetResult() == ESubmitResults::SUBMIT_SAVED || bSaveDescriptionOnSubmitFailure) { FChangeListDescription Description; SourceControlWidget->FillChangeListDescription(Description); EditChangelistDescription(Description.Description, ChangelistState); } if (bCheckinSuccess) { // Clear the description saved by the 'submit window'. Useful when the submit window is opened from the Editor menu rather than the changelist window. // Opening the 'submit window' from the Editor menu is intended for source controls that do not support changelists (SVN/Git), but remains available to // all source controls at the moment. SourceControlWidget->ClearChangeListDescription(); } } bool SSourceControlChangelistsWidget::CanSubmitChangelist() { return CanSubmitChangelist(/*OutFailureMessage*/nullptr); } bool SSourceControlChangelistsWidget::CanSubmitChangelist(FText* OutFailureMessage) { FSourceControlChangelistStatePtr Changelist = GetCurrentChangelistState(); if (Changelist == nullptr) { if (OutFailureMessage) { *OutFailureMessage = LOCTEXT("Cannot_Submit_Changelist_No_Selection", "No changelist selected."); } return false; } else if (Changelist->GetFilesStatesNum() <= 0) { if (OutFailureMessage) { *OutFailureMessage = LOCTEXT("Cannot_Submit_Changelist_No_Files", "The changelist doesn't contain any files to submit."); } return false; } else if (Changelist->GetShelvedFilesStatesNum() > 0) { if (OutFailureMessage) { *OutFailureMessage = LOCTEXT("Cannot_Submit_Changelist_Has_Shelved_Files", "The changelist contains shelved files."); } return false; } return true; } void SSourceControlChangelistsWidget::OnValidateChangelist() { FSourceControlChangelistStatePtr ChangelistState = GetCurrentChangelistState(); if (!ChangelistState) { return; } FString ChangelistValidationTitle; FString ChangelistValidationWarningsText; FString ChangelistValidationErrorsText; bool bValidationResult = GetChangelistValidationResult(ChangelistState->GetChangelist(), ChangelistValidationTitle, ChangelistValidationWarningsText, ChangelistValidationErrorsText); // Setup the notification for operation feedback FNotificationInfo Info(LOCTEXT("SCC_Validation_Success", "Changelist validated")); // Override the notification fields for failure ones if (!bValidationResult) { Info.Text = LOCTEXT("SCC_Validation_Failed", "Failed to validate the changelist"); } Info.ExpireDuration = 8.0f; Info.HyperlinkText = LOCTEXT("SCC_Validation_ShowLog", "Show Message Log"); Info.Hyperlink = FSimpleDelegate::CreateLambda([]() { FMessageLog("SourceControl").Open(EMessageSeverity::Info, true); }); TSharedPtr Notification = FSlateNotificationManager::Get().AddNotification(Info); Notification->SetCompletionState(bValidationResult ? SNotificationItem::CS_Success : SNotificationItem::CS_Fail); } bool SSourceControlChangelistsWidget::CanValidateChangelist() { FSourceControlChangelistStatePtr Changelist = GetCurrentChangelistState(); return Changelist != nullptr && Changelist->GetFilesStatesNum() > 0; } void SSourceControlChangelistsWidget::OnNewUncontrolledChangelist() { FText UncontrolledChangelistDescription; bool bOk = GetChangelistDescription( nullptr, LOCTEXT("SourceControl.UncontrolledChangelist.New.Title", "New Uncontrolled Changelist..."), LOCTEXT("SourceControl.UncontrolledChangelist.New.Label", "Enter a description for the uncontrolled changelist:"), UncontrolledChangelistDescription); if (!bOk) { return; } ExecuteUncontrolledChangelistOperation(LOCTEXT("Creating_Uncontrolled_Changelist", "Creating uncontrolled changelist..."), [&]() { FUncontrolledChangelistsModule::Get().CreateUncontrolledChangelist(UncontrolledChangelistDescription); }); } void SSourceControlChangelistsWidget::OnEditUncontrolledChangelist() { FUncontrolledChangelistStatePtr UncontrolledChangelistState = GetCurrentUncontrolledChangelistState(); if (UncontrolledChangelistState == nullptr) { return; } FText NewUncontrolledChangelistDescription = UncontrolledChangelistState->GetDisplayText(); bool bOk = GetChangelistDescription( nullptr, LOCTEXT("SourceControl.Uncontrolled.Changelist.New.Title2", "Edit Uncontrolled Changelist..."), LOCTEXT("SourceControl.Uncontrolled.Changelist.New.Label2", "Enter a new description for the uncontrolled changelist:"), NewUncontrolledChangelistDescription); if (!bOk) { return; } ExecuteUncontrolledChangelistOperation(LOCTEXT("Updating_Uncontrolled_Changelist_Description", "Updating uncontrolled changelist description..."), [&NewUncontrolledChangelistDescription, &UncontrolledChangelistState]() { FUncontrolledChangelistsModule::Get().EditUncontrolledChangelist(UncontrolledChangelistState->Changelist, NewUncontrolledChangelistDescription); }); } bool SSourceControlChangelistsWidget::CanEditUncontrolledChangelist() { FUncontrolledChangelistStatePtr UncontrolledChangelistState = GetCurrentUncontrolledChangelistState(); return (UncontrolledChangelistState != nullptr) && !UncontrolledChangelistState->Changelist.IsDefault(); } void SSourceControlChangelistsWidget::OnDeleteUncontrolledChangelist() { TOptional UncontrolledChangelist = GetCurrentUncontrolledChangelist(); if (!UncontrolledChangelist.IsSet()) { return; } ExecuteUncontrolledChangelistOperation(LOCTEXT("Deleting_Uncontrolled_Changelist", "Deleting uncontrolled changelist..."), [&UncontrolledChangelist]() { FUncontrolledChangelistsModule::Get().DeleteUncontrolledChangelist(UncontrolledChangelist.GetValue()); }); } bool SSourceControlChangelistsWidget::CanDeleteUncontrolledChangelist() { FUncontrolledChangelistStatePtr UncontrolledChangelistState = GetCurrentUncontrolledChangelistState(); return (UncontrolledChangelistState != nullptr) && !UncontrolledChangelistState->Changelist.IsDefault() && !UncontrolledChangelistState->ContainsFiles(); } TValueOrError SSourceControlChangelistsWidget::TryMoveFiles() { TArray SelectedControlledFiles; TArray SelectedUncontrolledFiles; GetSelectedFiles(SelectedControlledFiles, SelectedUncontrolledFiles); if (SelectedControlledFiles.IsEmpty() && SelectedUncontrolledFiles.IsEmpty()) { return MakeError(); } const bool bAddNewChangelistEntry = true; const bool bAddNewUncontrolledChangelistEntry = AreUncontrolledChangelistsEnabled(); // Build selection list for changelists TArray Items; Items.Reset(ChangelistTreeNodes.Num() + UncontrolledChangelistTreeNodes.Num() + (bAddNewChangelistEntry ? 1 : 0)); if (bAddNewChangelistEntry) { // First item in the 'Move To' list is always 'new changelist' Items.Emplace( LOCTEXT("SourceControl_NewChangelistText", "New Changelist"), LOCTEXT("SourceControl_NewChangelistDescription", ""), /*bCanEditDescription=*/true); } if (bAddNewUncontrolledChangelistEntry) { // Second item in the 'Move To' list is 'new uncontrolled changelist' (if enabled) Items.Emplace( LOCTEXT("SourceControl_NewUncontrolledChangelistText", "New Uncontrolled Changelist"), LOCTEXT("SourceControl_NewUncontrolledChangelistDescription", ""), /*bCanEditDescription=*/true); } const bool bCanEditAlreadyExistingChangelistDescription = false; for (TSharedPtr& Changelist : ChangelistTreeNodes) { if (Changelist->GetTreeItemType() == IChangelistTreeItem::Changelist) { const TSharedPtr& TypedChangelist = StaticCastSharedPtr(Changelist); Items.Emplace(TypedChangelist->GetDisplayText(), TypedChangelist->GetDescriptionText(), bCanEditAlreadyExistingChangelistDescription); } } for (TSharedPtr& UncontrolledChangelist : UncontrolledChangelistTreeNodes) { if (UncontrolledChangelist->GetTreeItemType() == IChangelistTreeItem::UncontrolledChangelist) { const TSharedPtr& TypedChangelist = StaticCastSharedPtr(UncontrolledChangelist); Items.Emplace(TypedChangelist->GetDisplayText(), FText(), bCanEditAlreadyExistingChangelistDescription); } } int32 PickedItem = 0; FText ChangelistDescription; bool bOk = PickChangelistOrNewWithDescription( nullptr, LOCTEXT("SourceControl.MoveFiles.Title", "Move Files To..."), LOCTEXT("SourceControl.MoveFIles.Label", "Target Changelist:"), Items, PickedItem, ChangelistDescription); if (!bOk) { return MakeError(); } ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); bool bFailed = false; // Move files to a new changelist if (bAddNewChangelistEntry && PickedItem == 0) { // NOTE: To perform async move, we would need to copy the list of selected uncontrolled files and ensure the list wasn't modified when callback occurs. For now run synchronously. TSharedRef NewChangelistOperation = ISourceControlOperation::Create(); NewChangelistOperation->SetDescription(ChangelistDescription); Execute(LOCTEXT("Moving_Files_New_Changelist", "Moving file(s) to a new changelist..."), NewChangelistOperation, SelectedControlledFiles, EConcurrency::Synchronous, FSourceControlOperationComplete::CreateLambda( [this, &SelectedUncontrolledFiles, &bFailed](const TSharedRef& Operation, ECommandResult::Type InResult) { if (InResult == ECommandResult::Succeeded) { // NOTE: Perform uncontrolled move only if the new changelist was created and the controlled file were move. if ((!SelectedUncontrolledFiles.IsEmpty()) && static_cast(Operation.Get()).GetNewChangelist().IsValid()) { FUncontrolledChangelistsModule::Get().MoveFilesToControlledChangelist(SelectedUncontrolledFiles, static_cast(Operation.Get()).GetNewChangelist(), SSourceControlCommon::OpenConflictDialog); } SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Move_Files_New_Changelist_Succeeded", "Files were successfully moved to a new changelist."), SNotificationItem::CS_Success); } if (InResult == ECommandResult::Failed) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Move_Files_New_Changelist_Failed", "Failed to move the file to the new changelist."), SNotificationItem::CS_Fail); bFailed = true; } })); } else if ((bAddNewUncontrolledChangelistEntry && bAddNewChangelistEntry && PickedItem == 1) || (bAddNewUncontrolledChangelistEntry && !bAddNewChangelistEntry && PickedItem == 0)) // Move files to a new uncontrolled changelist { ExecuteUncontrolledChangelistOperation(LOCTEXT("Moving_Files_New_Uncontrolled_Changelist", "Moving file(s) to a new uncontrolled changelist..."), [&]() { TArray SelectedControlledFileStates; TArray SelectedUnControlledFileStates; FUncontrolledChangelistsModule& UncontrolledChangelistsModule = FUncontrolledChangelistsModule::Get(); GetSelectedFileStates(SelectedControlledFileStates, SelectedUnControlledFileStates); TOptional NewUncontrolledChangelist = UncontrolledChangelistsModule.CreateUncontrolledChangelist(ChangelistDescription); if (!NewUncontrolledChangelist.IsSet()) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Move_Files_New_Uncontrolled_Changelist_Failed", "Failed to create a new uncontrolled changelist."), SNotificationItem::CS_Fail); bFailed = true; } else if (!SelectedControlledFileStates.IsEmpty() || !SelectedUnControlledFileStates.IsEmpty()) { UncontrolledChangelistsModule.MoveFilesToUncontrolledChangelist(SelectedControlledFileStates, SelectedUnControlledFileStates, NewUncontrolledChangelist.GetValue()); } // SelectedControlledFiles can contain UnsavedAssets when offline even if both sets of states are empty. else if (!SelectedControlledFiles.IsEmpty()) { UncontrolledChangelistsModule.MoveFilesToUncontrolledChangelist(SelectedControlledFiles, NewUncontrolledChangelist.GetValue()); } }); } else // Move files to an existing changelist or uncontrolled changelist. { // NOTE: The combo box indices are in this order: New changelist, existing changelist(s), existing uncontrolled changelist(s) FChangelistTreeItemPtr MoveDestination; int32 ChangelistIndex = (bAddNewChangelistEntry ? PickedItem - 1 : PickedItem); if (bAddNewUncontrolledChangelistEntry) { --ChangelistIndex; check(ChangelistIndex >= 0); } if (ChangelistIndex < ChangelistTreeNodes.Num()) // Move files to a changelist { MoveDestination = ChangelistTreeNodes[ChangelistIndex]; } else // Move files to an uncontrolled changelist. All uncontrolled CL were listed after the controlled CL in the combo box, compute the offset. { MoveDestination = UncontrolledChangelistTreeNodes[ChangelistIndex - ChangelistTreeNodes.Num()]; } // Move file to a changelist. if (MoveDestination->GetTreeItemType() == IChangelistTreeItem::Changelist) { FSourceControlChangelistPtr Changelist = StaticCastSharedPtr(MoveDestination)->ChangelistState->GetChangelist(); if (!SelectedControlledFiles.IsEmpty()) { Execute(LOCTEXT("Moving_File_Between_Changelists", "Moving file(s) to the selected changelist..."), ISourceControlOperation::Create(), Changelist, SelectedControlledFiles, EConcurrency::Synchronous, FSourceControlOperationComplete::CreateLambda( [&bFailed](const TSharedRef& Operation, ECommandResult::Type InResult) { if (InResult == ECommandResult::Succeeded) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Move_Files_Between_Changelist_Succeeded", "File(s) successfully moved to the selected changelist."), SNotificationItem::CS_Success); } else if (InResult == ECommandResult::Failed) { SSourceControlCommon::DisplaySourceControlOperationNotification(LOCTEXT("Move_Files_Between_Changelist_Failed", "Failed to move the file(s) to the selected changelist."), SNotificationItem::CS_Fail); bFailed = true; } })); } else if (!SelectedUncontrolledFiles.IsEmpty()) { ExecuteUncontrolledChangelistOperation(LOCTEXT("Moving_Uncontrolled_Files_To_Changelist", "Moving uncontrolled files..."), [&]() { FUncontrolledChangelistsModule::Get().MoveFilesToControlledChangelist(SelectedUncontrolledFiles, Changelist, SSourceControlCommon::OpenConflictDialog); }); } } else if (MoveDestination->GetTreeItemType() == IChangelistTreeItem::UncontrolledChangelist) { const FUncontrolledChangelist UncontrolledChangelist = StaticCastSharedPtr(MoveDestination)->UncontrolledChangelistState->Changelist; TArray SelectedControlledFileStates; TArray SelectedUnControlledFileStates; GetSelectedFileStates(SelectedControlledFileStates, SelectedUnControlledFileStates); ExecuteUncontrolledChangelistOperation(LOCTEXT("Moving_Uncontrolled_Changelist_To", "Moving uncontrolled files..."), [&]() { if ((!SelectedControlledFileStates.IsEmpty()) || (!SelectedUnControlledFileStates.IsEmpty())) { FUncontrolledChangelistsModule::Get().MoveFilesToUncontrolledChangelist(SelectedControlledFileStates, SelectedUnControlledFileStates, UncontrolledChangelist); } // SelectedControlledFiles can contain UnsavedAssets when offline even if both sets of states are empty. else if (!SelectedControlledFiles.IsEmpty()) { FUncontrolledChangelistsModule::Get().MoveFilesToUncontrolledChangelist(SelectedControlledFiles, UncontrolledChangelist); } }); } } if (bFailed) { return MakeError(); } return MakeValue(); } void SSourceControlChangelistsWidget::OnShowHistory() { TArray SelectedFiles = GetSelectedFiles(); if (SelectedFiles.Num() > 0) { FSourceControlWindows::DisplayRevisionHistory(SelectedFiles); } } void SSourceControlChangelistsWidget::OnDiffAgainstDepot() { TArray SelectedFiles = GetSelectedFiles(); if (SelectedFiles.Num() > 0) { FSourceControlWindows::DiffAgainstWorkspace(SelectedFiles[0]); } } bool SSourceControlChangelistsWidget::CanDiffAgainstDepot() { return FileListView->GetNumItemsSelected() == 1 && HasFilesSelected(); } void SSourceControlChangelistsWidget::OnDiffAgainstWorkspace() { if (GetSelectedShelvedFiles().Num() > 0) { FSourceControlStateRef FileState = StaticCastSharedPtr(FileListView->GetSelectedItems()[0])->FileState; FSourceControlWindows::DiffAgainstShelvedFile(FileState); } } bool SSourceControlChangelistsWidget::CanDiffAgainstWorkspace() { return FileListView->GetNumItemsSelected() == 1 && HasShelvedFilesSelected(); } TSharedPtr SSourceControlChangelistsWidget::OnOpenContextMenu() { UToolMenus* ToolMenus = UToolMenus::Get(); static const FName MenuName = "SourceControl.ChangelistContextMenu"; if (!ToolMenus->IsMenuRegistered(MenuName)) { UToolMenu* RegisteredMenu = ToolMenus->RegisterMenu(MenuName); // Add section so it can be used as insert position for menu extensions RegisteredMenu->AddSection("Source Control"); } TArray> SelectedChangelistNodes = ChangelistTreeView->GetSelectedItems(); TArray> SelectedUncontrolledChangelistNodes = UncontrolledChangelistTreeView->GetSelectedItems(); TArray> SelectedUnsavedAssetNodes = UnsavedAssetsTreeView->GetSelectedItems(); bool bHasSelectedChangelist = SelectedChangelistNodes.Num() > 0 && SelectedChangelistNodes[0]->GetTreeItemType() == IChangelistTreeItem::Changelist; bool bHasSelectedShelvedChangelistNode = SelectedChangelistNodes.Num() > 0 && SelectedChangelistNodes[0]->GetTreeItemType() == IChangelistTreeItem::ShelvedChangelist; bool bHasSelectedUncontrolledChangelist = SelectedUncontrolledChangelistNodes.Num() > 0 && SelectedUncontrolledChangelistNodes[0]->GetTreeItemType() == IChangelistTreeItem::UncontrolledChangelist; bool bHasSelectedUnsavedAsset = SelectedUnsavedAssetNodes.Num() > 0; bool bHasSelectedFiles = HasFilesSelected(); bool bHasSelectedShelvedFiles = HasShelvedFilesSelected(); bool bHasEmptySelection = (!bHasSelectedChangelist && !bHasSelectedFiles && !bHasSelectedShelvedFiles); // Build up the menu for a selection USourceControlMenuContext* ContextObject = NewObject(); FToolMenuContext Context(ContextObject); // Fill Context Object TArray SelectedControlledFiles; TArray SelectedUncontrolledFiles; GetSelectedFiles(SelectedControlledFiles, SelectedUncontrolledFiles); ContextObject->SelectedFiles.Append(SelectedControlledFiles); ContextObject->SelectedFiles.Append(SelectedUncontrolledFiles); ContextObject->SelectedChangelist = GetChangelistFromSelection(); UToolMenu* Menu = ToolMenus->GenerateMenu(MenuName, Context); FToolMenuSection& Section = *Menu->FindSection("Source Control"); if (bHasSelectedUnsavedAsset) { Section.AddMenuEntry( "Save", LOCTEXT("SourceControl_SaveUnsavedAsset", "Save"), FText::FormatOrdered(LOCTEXT("SourceControl_SaveUnsavedAsset_Tooltip", "Save unsaved {0}|plural(one=asset,other=assets)"), ContextObject->SelectedFiles.Num()), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Save"), FUIAction(FExecuteAction::CreateLambda([this] { TArray SelectedFiles; GetSelectedFiles(SelectedFiles, SelectedFiles); TArray Packages; Packages.Reserve(SelectedFiles.Num()); { for (const FString& Filename : SelectedFiles) { FString PackageName = UPackageTools::FilenameToPackageName(Filename); UPackage* Package = FindPackage(nullptr, *PackageName); if (ensure(Package)) { Packages.Add(Package); } else { Packages.Add(UPackageTools::LoadPackage(PackageName)); } } } const bool bOnlyDirty = false; UEditorLoadingAndSavingUtils::SavePackages(Packages, bOnlyDirty); // Request a changelists refresh in the next tick bShouldRefresh = true; }), FCanExecuteAction::CreateLambda([this] { return HasFilesSelected(); }))); Section.AddMenuEntry( "SaveToChangelist", LOCTEXT("SourceControl_SaveUnsavedAssetToChangelist", "Save To Changelist"), FText::FormatOrdered(LOCTEXT("SourceControl_SaveUnsavedAssetToChangelist_Tooltip", "Save {0}|plural(one=this asset,other=these assets) and add {0}|plural(one=it,other=them) to a changelist"), ContextObject->SelectedFiles.Num()), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.ArrowRight"), FUIAction(FExecuteAction::CreateLambda([this] { FPackageSourceControlHelper PackageHelper; TArray SelectedFiles; GetSelectedFiles(SelectedFiles, SelectedFiles); TArray PackageNames; for (const FString& Filename : SelectedFiles) { PackageNames.Add(UPackageTools::FilenameToPackageName(Filename)); } if (!PackageHelper.Checkout(PackageNames)) { return; } // Actually save the assets TArray Packages; Packages.Reserve(PackageNames.Num()); { for (const FString& PackageName : PackageNames) { UPackage* Package = FindPackage(nullptr, *PackageName); if (ensure(Package)) { Packages.Add(Package); } else { Packages.Add(UPackageTools::LoadPackage(PackageName)); } } } const bool bOnlyDirty = false; UEditorLoadingAndSavingUtils::SavePackages(Packages, bOnlyDirty); // Move the files only after saving the packages. // If we are moving files to an uncontrolled changelist, they will be reverted from source control during the move. TryMoveFiles(); // Request a changelists refresh in the next tick bShouldRefresh = true; }), FCanExecuteAction::CreateLambda([this] { return HasFilesSelected(); }))); Section.AddSeparator("ActionSeparator"); } // This should appear only on change lists if (bHasSelectedChangelist) { Section.AddMenuEntry( "SubmitChangelist", LOCTEXT("SourceControl_SubmitChangelist", "Submit Changelist..."), LOCTEXT("SourceControl_SubmitChangeslit_Tooltip", "Submits a changelist"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::OnSubmitChangelist), FCanExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::CanSubmitChangelist))); Section.AddMenuEntry( "ValidateChangelist", LOCTEXT("SourceControl_ValidateChangelist", "Validate Changelist"), LOCTEXT("SourceControl_ValidateChangeslit_Tooltip", "Validates a changelist"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::OnValidateChangelist), FCanExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::CanValidateChangelist))); Section.AddMenuEntry( "RevertUnchanged", LOCTEXT("SourceControl_RevertUnchanged", "Revert Unchanged"), LOCTEXT("SourceControl_Revert_Unchanged_Tooltip", "Reverts unchanged files & changelists"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::OnRevertUnchanged), FCanExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::CanRevertUnchanged))); } if (bHasSelectedChangelist || bHasSelectedUncontrolledChangelist) { Section.AddMenuEntry( "Revert", LOCTEXT("SourceControl_Revert", "Revert Files"), LOCTEXT("SourceControl_Revert_Tooltip", "Reverts all files in the changelist or from the selection"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::OnRevert), FCanExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::CanRevert))); } if (bHasSelectedChangelist && (bHasSelectedFiles || bHasSelectedShelvedFiles || (bHasSelectedChangelist && (GetCurrentChangelistState()->GetFilesStatesNum() > 0 || GetCurrentChangelistState()->GetShelvedFilesStates().Num() > 0)))) { Section.AddSeparator("ShelveSeparator"); } if (bHasSelectedChangelist && (bHasSelectedFiles || (bHasSelectedChangelist && GetCurrentChangelistState()->GetFilesStatesNum() > 0))) { Section.AddMenuEntry("Shelve", LOCTEXT("SourceControl_Shelve", "Shelve Files"), LOCTEXT("SourceControl_Shelve_Tooltip", "Shelves the changelist or the selected files"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::OnShelve))); } if (bHasSelectedShelvedFiles || bHasSelectedShelvedChangelistNode) { Section.AddMenuEntry( "Unshelve", LOCTEXT("SourceControl_Unshelve", "Unshelve Files"), LOCTEXT("SourceControl_Unshelve_Tooltip", "Unshelve selected files or changelist"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::OnUnshelve))); Section.AddMenuEntry( "DeleteShelved", LOCTEXT("SourceControl_DeleteShelved", "Delete Shelved Files"), LOCTEXT("SourceControl_DeleteShelved_Tooltip", "Delete selected shelved files or all from changelist"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::OnDeleteShelvedFiles))); } // Shelved files-only operations if (bHasSelectedShelvedFiles) { // Diff against workspace Section.AddMenuEntry( "DiffAgainstWorkspace", LOCTEXT("SourceControl_DiffAgainstWorkspace", "Diff Against Workspace Files..."), LOCTEXT("SourceControl_DiffAgainstWorkspace_Tooltip", "Diff shelved file against the (local) workspace file"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::OnDiffAgainstWorkspace), FCanExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::CanDiffAgainstWorkspace))); } if (bHasEmptySelection || bHasSelectedChangelist) { Section.AddSeparator("ChangelistsSeparator"); } if (bHasSelectedChangelist) { Section.AddMenuEntry( "EditChangelist", LOCTEXT("SourceControl_EditChangelist", "Edit Changelist..."), LOCTEXT("SourceControl_Edit_Changelist_Tooltip", "Edit a changelist description"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::OnEditChangelist))); Section.AddMenuEntry( "DeleteChangelist", LOCTEXT("SourceControl_DeleteChangelist", "Delete Empty Changelist"), LOCTEXT("SourceControl_Delete_Changelist_Tooltip", "Deletes an empty changelist"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::OnDeleteChangelist), FCanExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::CanDeleteChangelist))); } if (bHasSelectedUncontrolledChangelist) { Section.AddMenuEntry( "EditUncontrolledChangelist", LOCTEXT("SourceControl_EditUncontrolledChangelist", "Edit Uncontrolled Changelist..."), LOCTEXT("SourceControl_Edit_Uncontrolled_Changelist_Tooltip", "Edit an uncontrolled changelist description"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::OnEditUncontrolledChangelist), FCanExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::CanEditUncontrolledChangelist))); Section.AddMenuEntry( "DeleteUncontrolledChangelist", LOCTEXT("SourceControl_DeleteUncontrolledChangelist", "Delete Empty Changelist"), LOCTEXT("SourceControl_Delete_Uncontrolled_Changelist_Tooltip", "Deletes an empty uncontrolled changelist"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::OnDeleteUncontrolledChangelist), FCanExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::CanDeleteUncontrolledChangelist))); } // Files-only operations if(bHasSelectedFiles && !bHasSelectedUnsavedAsset) { Section.AddSeparator("FilesSeparator"); Section.AddMenuEntry( "MoveFiles", LOCTEXT("SourceControl_MoveFiles", "Move Files To..."), LOCTEXT("SourceControl_MoveFiles_Tooltip", "Move Files To A Different Changelist..."), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda([this] { TryMoveFiles(); }))); Section.AddMenuEntry( "ShowHistory", LOCTEXT("SourceControl_ShowHistory", "Show History..."), LOCTEXT("SourceControl_ShowHistory_ToolTip", "Show File History From Selection..."), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::OnShowHistory))); Section.AddMenuEntry( "DiffAgainstLocalVersion", LOCTEXT("SourceControl_DiffAgainstDepot", "Diff Against Depot..."), LOCTEXT("SourceControl_DiffAgainstLocal_Tooltip", "Diff local file against depot revision."), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::OnDiffAgainstDepot), FCanExecuteAction::CreateSP(this, &SSourceControlChangelistsWidget::CanDiffAgainstDepot))); } if (FUncontrolledChangelistsModule::Get().IsEnabled() && !bHasSelectedUnsavedAsset) { Section.AddSeparator("ReconcileSeparator"); Section.AddMenuEntry( "Reconcile assets", LOCTEXT("SourceControl_ReconcileAssets", "Reconcile assets"), LOCTEXT("SourceControl_ReconcileAssets_Tooltip", "Look for uncontrolled modification in currently added assets."), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda([]() { FUncontrolledChangelistsModule::Get().OnReconcileAssets(); }))); } return ToolMenus->GenerateWidget(Menu); } TSharedRef> SSourceControlChangelistsWidget::CreateChangelistTreeView(TArray>& ItemSources) { return SNew(STreeView) .TreeItemsSource(&ItemSources) .OnGenerateRow(this, &SSourceControlChangelistsWidget::OnGenerateRow) .OnGetChildren(this, &SSourceControlChangelistsWidget::OnGetChangelistChildren) .SelectionMode(ESelectionMode::Single) .OnMouseButtonDoubleClick(this, &SSourceControlChangelistsWidget::OnItemDoubleClicked) .OnContextMenuOpening(this, &SSourceControlChangelistsWidget::OnOpenContextMenu) .OnSelectionChanged(this, &SSourceControlChangelistsWidget::OnChangelistSelectionChanged); } TSharedRef> SSourceControlChangelistsWidget::CreateChangelistFilesView() { USourceControlSettings* Settings = GetMutableDefault(); if (!Settings->bShowAssetTypeColumn) { FileViewHiddenColumnsList.Add(SourceControlFileViewColumn::Type::Id()); } if (!Settings->bShowAssetLastModifiedTimeColumn) { FileViewHiddenColumnsList.Add(SourceControlFileViewColumn::LastModifiedTimestamp::Id()); } if (!Settings->bShowAssetCheckedOutByColumn) { FileViewHiddenColumnsList.Add(SourceControlFileViewColumn::CheckedOutByUser::Id()); } TSharedRef> FileView = SNew(SListView) .ListItemsSource(&FileListNodes) .OnGenerateRow(this, &SSourceControlChangelistsWidget::OnGenerateRow) .SelectionMode(ESelectionMode::Multi) .OnContextMenuOpening(this, &SSourceControlChangelistsWidget::OnOpenContextMenu) .OnMouseButtonDoubleClick(this, &SSourceControlChangelistsWidget::OnItemDoubleClicked) .OnListViewScrolled_Lambda([this](double InScrollOffset) { if (!IsFileViewSortedByFileStatusIcon()) { bUpdateMonitoredFileStatusList = true; } }) // If sorted by status icon, the full list of file is already monitored. .OnItemToString_Debug_Lambda([this](FChangelistTreeItemPtr Item) { return static_cast(Item.Get())->GetName(); }) .HeaderRow ( SNew(SHeaderRow) .CanSelectGeneratedColumn(true) .HiddenColumnsList(FileViewHiddenColumnsList) .OnHiddenColumnsListChanged(this, &SSourceControlChangelistsWidget::OnFileViewHiddenColumnsListChanged) +SHeaderRow::Column(SourceControlFileViewColumn::Icon::Id()) .DefaultLabel(SourceControlFileViewColumn::Icon::GetDisplayText()) // Displayed in the drop down menu to show/hide columns .DefaultTooltip(SourceControlFileViewColumn::Icon::GetToolTipText()) .ShouldGenerateWidget(true) // Ensure the column cannot be hidden (grayed out in the show/hide drop down menu) .FillSized(18) .HeaderContentPadding(FMargin(0)) .SortPriority(this, &SSourceControlChangelistsWidget::GetColumnSortPriority, SourceControlFileViewColumn::Icon::Id()) .SortMode(this, &SSourceControlChangelistsWidget::GetColumnSortMode, SourceControlFileViewColumn::Icon::Id()) .OnSort(this, &SSourceControlChangelistsWidget::OnColumnSortModeChanged) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .Padding(1, 0) [ SNew(SBox) .WidthOverride(16) .HeightOverride(16) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .Visibility_Lambda([this](){ return GetColumnSortMode(SourceControlFileViewColumn::Icon::Id()) == EColumnSortMode::None ? EVisibility::Visible : EVisibility::Collapsed; }) [ SNew(SImage) .Image(FRevisionControlStyleManager::Get().GetBrush("RevisionControl.ChangelistsTab")) .ColorAndOpacity(FSlateColor::UseSubduedForeground()) ] ] ] +SHeaderRow::Column(SourceControlFileViewColumn::Shelve::Id()) .DefaultLabel(SourceControlFileViewColumn::Shelve::GetDisplayText()) // Displayed in the drop down menu to show/hide columns .DefaultTooltip(SourceControlFileViewColumn::Shelve::GetToolTipText()) .ShouldGenerateWidget(true) // Ensure the column cannot be hidden (grayed out in the show/hide drop down menu) .FillSized(18) .HeaderContentPadding(FMargin(0)) .SortPriority(this, &SSourceControlChangelistsWidget::GetColumnSortPriority, SourceControlFileViewColumn::Shelve::Id()) .SortMode(this, &SSourceControlChangelistsWidget::GetColumnSortMode, SourceControlFileViewColumn::Shelve::Id()) .OnSort(this, &SSourceControlChangelistsWidget::OnColumnSortModeChanged) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .Padding(1, 0) [ SNew(SBox) .WidthOverride(16) .HeightOverride(16) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .Visibility_Lambda([this](){ return GetColumnSortMode(SourceControlFileViewColumn::Shelve::Id()) == EColumnSortMode::None ? EVisibility::Visible : EVisibility::Collapsed; }) [ SNew(SImage) .Image(FRevisionControlStyleManager::Get().GetBrush("RevisionControl.Shelved")) .ColorAndOpacity(FSlateColor::UseSubduedForeground()) ] ] ] +SHeaderRow::Column(SourceControlFileViewColumn::Name::Id()) .DefaultLabel(SourceControlFileViewColumn::Name::GetDisplayText()) .DefaultTooltip(SourceControlFileViewColumn::Name::GetToolTipText()) .ShouldGenerateWidget(true) // Ensure the column cannot be hidden (grayed out in the show/hide drop down menu) .FillWidth(1.5f) .SortPriority(this, &SSourceControlChangelistsWidget::GetColumnSortPriority, SourceControlFileViewColumn::Name::Id()) .SortMode(this, &SSourceControlChangelistsWidget::GetColumnSortMode, SourceControlFileViewColumn::Name::Id()) .OnSort(this, &SSourceControlChangelistsWidget::OnColumnSortModeChanged) +SHeaderRow::Column(SourceControlFileViewColumn::Path::Id()) .DefaultLabel(SourceControlFileViewColumn::Path::GetDisplayText()) .DefaultTooltip(SourceControlFileViewColumn::Path::GetToolTipText()) .ShouldGenerateWidget(true) // Ensure the column cannot be hidden (grayed out in the show/hide drop down menu) .FillWidth(3.5f) .SortPriority(this, &SSourceControlChangelistsWidget::GetColumnSortPriority, SourceControlFileViewColumn::Path::Id()) .SortMode(this, &SSourceControlChangelistsWidget::GetColumnSortMode, SourceControlFileViewColumn::Path::Id()) .OnSort(this, &SSourceControlChangelistsWidget::OnColumnSortModeChanged) +SHeaderRow::Column(SourceControlFileViewColumn::Type::Id()) .DefaultLabel(SourceControlFileViewColumn::Type::GetDisplayText()) .DefaultTooltip(SourceControlFileViewColumn::Type::GetToolTipText()) .FillWidth(2.0f) .SortPriority(this, &SSourceControlChangelistsWidget::GetColumnSortPriority, SourceControlFileViewColumn::Type::Id()) .SortMode(this, &SSourceControlChangelistsWidget::GetColumnSortMode, SourceControlFileViewColumn::Type::Id()) .OnSort(this, &SSourceControlChangelistsWidget::OnColumnSortModeChanged) +SHeaderRow::Column(SourceControlFileViewColumn::LastModifiedTimestamp::Id()) .DefaultLabel(SourceControlFileViewColumn::LastModifiedTimestamp::GetDisplayText()) .DefaultTooltip(SourceControlFileViewColumn::LastModifiedTimestamp::GetToolTipText()) .FillWidth(1.5f) .SortPriority(this, &SSourceControlChangelistsWidget::GetColumnSortPriority, SourceControlFileViewColumn::LastModifiedTimestamp::Id()) .SortMode(this, &SSourceControlChangelistsWidget::GetColumnSortMode, SourceControlFileViewColumn::LastModifiedTimestamp::Id()) .OnSort(this, &SSourceControlChangelistsWidget::OnColumnSortModeChanged) +SHeaderRow::Column(SourceControlFileViewColumn::CheckedOutByUser::Id()) .DefaultLabel(SourceControlFileViewColumn::CheckedOutByUser::GetDisplayText()) .DefaultTooltip(SourceControlFileViewColumn::CheckedOutByUser::GetToolTipText()) .FillWidth(1.0f) .SortPriority(this, &SSourceControlChangelistsWidget::GetColumnSortPriority, SourceControlFileViewColumn::CheckedOutByUser::Id()) .SortMode(this, &SSourceControlChangelistsWidget::GetColumnSortMode, SourceControlFileViewColumn::CheckedOutByUser::Id()) .OnSort(this, &SSourceControlChangelistsWidget::OnColumnSortModeChanged)); return FileView; } TSharedRef> SSourceControlChangelistsWidget::CreateUnsavedAssetsFilesView() { TSharedRef> FileView = SNew(SListView) .ListItemsSource(&FileListNodes) .OnGenerateRow(this, &SSourceControlChangelistsWidget::OnGenerateRow) .SelectionMode(ESelectionMode::Multi) .OnContextMenuOpening(this, &SSourceControlChangelistsWidget::OnOpenContextMenu) .HeaderRow ( SNew(SHeaderRow) .CanSelectGeneratedColumn(true) .HiddenColumnsList(FileViewHiddenColumnsList) .OnHiddenColumnsListChanged(this, &SSourceControlChangelistsWidget::OnFileViewHiddenColumnsListChanged) +SHeaderRow::Column(SourceControlFileViewColumn::Name::Id()) .DefaultLabel(SourceControlFileViewColumn::Name::GetDisplayText()) .DefaultTooltip(SourceControlFileViewColumn::Name::GetToolTipText()) .ShouldGenerateWidget(true) // Ensure the column cannot be hidden (grayed out in the show/hide drop down menu) .FillWidth(1.5f) .SortPriority(this, &SSourceControlChangelistsWidget::GetColumnSortPriority, SourceControlFileViewColumn::Name::Id()) .SortMode(this, &SSourceControlChangelistsWidget::GetColumnSortMode, SourceControlFileViewColumn::Name::Id()) .OnSort(this, &SSourceControlChangelistsWidget::OnColumnSortModeChanged) +SHeaderRow::Column(SourceControlFileViewColumn::Path::Id()) .DefaultLabel(SourceControlFileViewColumn::Path::GetDisplayText()) .DefaultTooltip(SourceControlFileViewColumn::Path::GetToolTipText()) .ShouldGenerateWidget(true) // Ensure the column cannot be hidden (grayed out in the show/hide drop down menu) .FillWidth(3.5f) .SortPriority(this, &SSourceControlChangelistsWidget::GetColumnSortPriority, SourceControlFileViewColumn::Path::Id()) .SortMode(this, &SSourceControlChangelistsWidget::GetColumnSortMode, SourceControlFileViewColumn::Path::Id()) .OnSort(this, &SSourceControlChangelistsWidget::OnColumnSortModeChanged) +SHeaderRow::Column(SourceControlFileViewColumn::Type::Id()) .DefaultLabel(SourceControlFileViewColumn::Type::GetDisplayText()) .DefaultTooltip(SourceControlFileViewColumn::Type::GetToolTipText()) .ShouldGenerateWidget(true) // Ensure the column cannot be hidden (grayed out in the show/hide drop down menu) .FillWidth(3.5f) .SortPriority(this, &SSourceControlChangelistsWidget::GetColumnSortPriority, SourceControlFileViewColumn::Type::Id()) .SortMode(this, &SSourceControlChangelistsWidget::GetColumnSortMode, SourceControlFileViewColumn::Type::Id()) .OnSort(this, &SSourceControlChangelistsWidget::OnColumnSortModeChanged) +SHeaderRow::Column(SourceControlFileViewColumn::Discard::Id()) .DefaultLabel(SourceControlFileViewColumn::Discard::GetDisplayText()) .DefaultTooltip(SourceControlFileViewColumn::Discard::GetToolTipText()) .ShouldGenerateWidget(true) // Ensure the column cannot be hidden (grayed out in the show/hide drop down menu) .FixedWidth(18) .HeaderContentPadding(FMargin(0)) .HAlignHeader(HAlign_Right) [ SNew(SBox) .WidthOverride(18) .HeightOverride(16) ]); return FileView; } void SSourceControlChangelistsWidget::OnFileViewHiddenColumnsListChanged() { if (FileListView && FileListView->GetHeaderRow()) { USourceControlSettings* Settings = GetMutableDefault(); Settings->bShowAssetTypeColumn = true; Settings->bShowAssetCheckedOutByColumn = true; Settings->bShowAssetLastModifiedTimeColumn = true; for (const FName& ColumnId : FileListView->GetHeaderRow()->GetHiddenColumnIds()) { if (ColumnId == SourceControlFileViewColumn::Type::Id()) { Settings->bShowAssetTypeColumn = false; } else if (ColumnId == SourceControlFileViewColumn::LastModifiedTimestamp::Id()) { Settings->bShowAssetLastModifiedTimeColumn = false; } else if (ColumnId == SourceControlFileViewColumn::CheckedOutByUser::Id()) { Settings->bShowAssetCheckedOutByColumn = false; } } Settings->SaveConfig(); } } TSharedRef SSourceControlChangelistsWidget::OnGenerateRow(TSharedPtr InTreeItem, const TSharedRef& OwnerTable) { switch (InTreeItem->GetTreeItemType()) { case IChangelistTreeItem::Changelist: return SNew(SChangelistTableRow, OwnerTable) .TreeItemToVisualize(InTreeItem) .HighlightText_Lambda([this]() { return ChangelistExpandableArea->GetSearchedText(); }) .OnPostDrop_Lambda([this] { OnRefreshUI(ERefreshFlags::All); }); case IChangelistTreeItem::UncontrolledChangelist: return SNew(SUncontrolledChangelistTableRow, OwnerTable) .TreeItemToVisualize(InTreeItem) .HighlightText_Lambda([this]() { return UncontrolledChangelistExpandableArea->GetSearchedText(); }) .OnPostDrop_Lambda([this] { OnRefreshUI(ERefreshFlags::All); }); case IChangelistTreeItem::File: bUpdateMonitoredFileStatusList = true; return SNew(SFileTableRow, OwnerTable) .TreeItemToVisualize(InTreeItem) .HighlightText_Lambda([this]() { return FileSearchBox->GetText(); }) .PathFlags_Lambda([this]() { return bShowingContentVersePath ? SourceControlFileViewColumn::EPathFlags::ShowingVersePath : SourceControlFileViewColumn::EPathFlags::Default; }) .OnDragDetected(this, &SSourceControlChangelistsWidget::OnFilesDragged); case IChangelistTreeItem::OfflineFile: bUpdateMonitoredFileStatusList = true; return SNew(SOfflineFileTableRow, OwnerTable) .TreeItemToVisualize(InTreeItem) .HighlightText_Lambda([this]() { return FileSearchBox->GetText(); }) .PathFlags_Lambda([this]() { return bShowingContentVersePath ? SourceControlFileViewColumn::EPathFlags::ShowingVersePath : SourceControlFileViewColumn::EPathFlags::Default; }) .OnDragDetected(this, &SSourceControlChangelistsWidget::OnUnsavedAssetsDragged); case IChangelistTreeItem::ShelvedChangelist: return SNew(SShelvedFilesTableRow, OwnerTable) .TreeItemToVisualize(InTreeItem) .HighlightText_Lambda([this]() { return ChangelistExpandableArea->GetSearchedText(); }); case IChangelistTreeItem::ShelvedFile: bUpdateMonitoredFileStatusList = true; return SNew(SFileTableRow, OwnerTable) .TreeItemToVisualize(InTreeItem) .HighlightText_Lambda([this]() { return FileSearchBox->GetText(); }) .PathFlags_Lambda([this]() { return bShowingContentVersePath ? SourceControlFileViewColumn::EPathFlags::ShowingVersePath : SourceControlFileViewColumn::EPathFlags::Default; }); case IChangelistTreeItem::UnsavedAssets: return SNew(SUnsavedAssetsTableRow, OwnerTable); default: checkNoEntry(); }; return SNew(STableRow>, OwnerTable); } FReply SSourceControlChangelistsWidget::OnFilesDragged(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) { if (InMouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton) && !FileListView->GetSelectedItems().IsEmpty()) { TSharedRef Operation = MakeShared(); for (const FChangelistTreeItemPtr& InTreeItem : FileListView->GetSelectedItems()) { if (InTreeItem->GetTreeItemType() == IChangelistTreeItem::File) { TSharedRef FileTreeItem = StaticCastSharedRef(InTreeItem.ToSharedRef()); FSourceControlStateRef FileState = FileTreeItem->FileState; if (FileTreeItem->GetParent()->GetTreeItemType() == IChangelistTreeItem::UncontrolledChangelist) { Operation->UncontrolledFiles.Add(MoveTemp(FileState)); } else { Operation->Files.Add(MoveTemp(FileState)); } } } Operation->Construct(); return FReply::Handled().BeginDragDrop(Operation); } return FReply::Unhandled(); } FReply SSourceControlChangelistsWidget::OnUnsavedAssetsDragged(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) { if (InMouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton) && !UnsavedAssetsFileListView->GetSelectedItems().IsEmpty()) { TSharedRef Operation = MakeShared(); for (const FChangelistTreeItemPtr& InTreeItem : UnsavedAssetsFileListView->GetSelectedItems()) { if (InTreeItem->GetTreeItemType() == IChangelistTreeItem::OfflineFile) { TSharedRef FileTreeItem = StaticCastSharedRef(InTreeItem.ToSharedRef()); Operation->OfflineFiles.Emplace(FileTreeItem->GetFilename()); } } Operation->Construct(); // Initiates drag-drop operation. return FReply::Handled().BeginDragDrop(Operation); } return FReply::Unhandled(); } void SSourceControlChangelistsWidget::OnGetChangelistChildren(FChangelistTreeItemPtr InParent, TArray& OutChildren) { if (InParent->GetTreeItemType() == IChangelistTreeItem::Changelist) { const FChangelistTreeItem* ChangelistItem = static_cast(InParent.Get()); if (ChangelistItem->GetShelvedFileCount() > 0 && ChangelistItem->ShelvedChangelistItem->GetParent() != nullptr) { OutChildren.Add(ChangelistItem->ShelvedChangelistItem); // Add the 'Shelved Files' only if is is linked to its parent (has items and is not filtered out). } } else if (InParent->GetTreeItemType() == IChangelistTreeItem::UncontrolledChangelist) { // Uncontrolled changelist nodes do not have children at the moment. } else if (InParent->GetTreeItemType() == IChangelistTreeItem::UnsavedAssets) { // The unsaved assets node does not have children at the moment. } } void SSourceControlChangelistsWidget::OnItemDoubleClicked(TSharedPtr Item) { if (Item->GetTreeItemType() == IChangelistTreeItem::OfflineFile) { const FString& Filename = StaticCastSharedPtr(Item)->GetFilename(); ISourceControlWindowsModule::Get().OnChangelistFileDoubleClicked().Broadcast(Filename); } else if (Item->GetTreeItemType() == IChangelistTreeItem::File) { const FString& Filename = StaticCastSharedPtr(Item)->FileState->GetFilename(); ISourceControlWindowsModule::Get().OnChangelistFileDoubleClicked().Broadcast(Filename); } else if (Item->GetTreeItemType() == IChangelistTreeItem::Changelist) { // Submit the currently selected changelists if conditions are met. FText FailureMessage; if (CanSubmitChangelist(&FailureMessage)) { OnSubmitChangelist(); } else { FMessageDialog::Open(EAppMsgType::Ok, EAppReturnType::Ok, FailureMessage, LOCTEXT("Cannot_Submit_Changelist_Title", "Cannot Submit Changelist")); } } } void SSourceControlChangelistsWidget::OnChangelistSelectionChanged(TSharedPtr SelectedChangelist, ESelectInfo::Type SelectionType) { TRACE_CPUPROFILER_EVENT_SCOPE(SSourceControlChangelistsWidget::OnChangelistSelectionChanged); FileListNodes.Reset(); GetActiveFileListView().RequestListRefresh(); bUpdateMonitoredFileStatusList = true; if (SelectedChangelist) // Can be a Changelist, Uncontrolled Changelist or Shelved Changelist { IChangelistTreeItem::TreeItemType ChangelistType = SelectedChangelist->GetTreeItemType(); switch (ChangelistType) { case IChangelistTreeItem::Changelist: case IChangelistTreeItem::ShelvedChangelist: UncontrolledChangelistTreeView->ClearSelection(); // Don't have a changelists selected at the same time than an uncontrolled one, they share the same file view. UnsavedAssetsTreeView->ClearSelection(); FileListViewSwitcher->SetActiveWidget(FileListView.ToSharedRef()); OnRefreshUI(ERefreshFlags::SourceControlChangelists); break; case IChangelistTreeItem::UncontrolledChangelist: ChangelistTreeView->ClearSelection(); UnsavedAssetsTreeView->ClearSelection(); FileListViewSwitcher->SetActiveWidget(FileListView.ToSharedRef()); OnRefreshUI(ERefreshFlags::UncontrolledChangelists); break; case IChangelistTreeItem::UnsavedAssets: ChangelistTreeView->ClearSelection(); UncontrolledChangelistTreeView->ClearSelection(); FileListViewSwitcher->SetActiveWidget(UnsavedAssetsFileListView.ToSharedRef()); OnRefreshUI(ERefreshFlags::UnsavedAssets); break; default: break; } } } EColumnSortPriority::Type SSourceControlChangelistsWidget::GetColumnSortPriority(const FName ColumnId) const { if (ColumnId == PrimarySortedColumn) { return EColumnSortPriority::Primary; } else if (ColumnId == SecondarySortedColumn) { return EColumnSortPriority::Secondary; } return EColumnSortPriority::Max; // No specific priority. } EColumnSortMode::Type SSourceControlChangelistsWidget::GetColumnSortMode(const FName ColumnId) const { if (ColumnId == PrimarySortedColumn) { return PrimarySortMode; } else if (ColumnId == SecondarySortedColumn) { return SecondarySortMode; } return EColumnSortMode::None; } void SSourceControlChangelistsWidget::OnColumnSortModeChanged(const EColumnSortPriority::Type InSortPriority, const FName& InColumnId, const EColumnSortMode::Type InSortMode) { if (InSortPriority == EColumnSortPriority::Primary) { PrimarySortedColumn = InColumnId; PrimarySortMode = InSortMode; if (InColumnId == SecondarySortedColumn) // Cannot be primary and secondary at the same time. { SecondarySortedColumn = FName(); SecondarySortMode = EColumnSortMode::None; } } else if (InSortPriority == EColumnSortPriority::Secondary) { SecondarySortedColumn = InColumnId; SecondarySortMode = InSortMode; } if (IsFileViewSortedByFileStatusIcon() || IsFileViewSortedByFileShelveIcon()) { bUpdateMonitoredFileStatusList = true; } SortFileView(); GetActiveFileListView().RequestListRefresh(); } bool SSourceControlChangelistsWidget::IsFileViewSortedByFileStatusIcon() const { return PrimarySortedColumn == SourceControlFileViewColumn::Icon::Id() || SecondarySortedColumn == SourceControlFileViewColumn::Icon::Id(); }; bool SSourceControlChangelistsWidget::IsFileViewSortedByFileShelveIcon() const { return PrimarySortedColumn == SourceControlFileViewColumn::Shelve::Id() || SecondarySortedColumn == SourceControlFileViewColumn::Shelve::Id(); }; bool SSourceControlChangelistsWidget::IsFileViewSortedByLastModifiedTimestamp() const { return PrimarySortedColumn == SourceControlFileViewColumn::LastModifiedTimestamp::Id() || SecondarySortedColumn == SourceControlFileViewColumn::LastModifiedTimestamp::Id(); } void SSourceControlChangelistsWidget::SortFileView() { TRACE_CPUPROFILER_EVENT_SCOPE(SSourceControlChangelistsWidget::SortFileView); if (PrimarySortedColumn.IsNone() || FileListNodes.IsEmpty()) { return; // No column selected for sorting or nothing to sort. } const SourceControlFileViewColumn::EPathFlags PathFlags = bShowingContentVersePath ? SourceControlFileViewColumn::EPathFlags::ShowingVersePath : SourceControlFileViewColumn::EPathFlags::Default; TFunction PrimaryCompare = SourceControlFileViewColumn::GetColumnComparer(PrimarySortedColumn, PathFlags); TFunction SecondaryCompare; if (!SecondarySortedColumn.IsNone()) { SecondaryCompare = SourceControlFileViewColumn::GetColumnComparer(SecondarySortedColumn, PathFlags); } if (PrimarySortMode == EColumnSortMode::Ascending) { // NOTE: StableSort() would give a better experience when the sorted columns(s) has the same values and new values gets added, but it is slower // with large changelists (7600 items was about 1.8x slower in average measured with Unreal Insight). Because this code runs in the main // thread and can be invoked a lot, the trade off went if favor of speed. FileListNodes.Sort([this, &PrimaryCompare, &SecondaryCompare](const TSharedPtr& Lhs, const TSharedPtr& Rhs) { int32 Result = PrimaryCompare(static_cast(*Lhs), static_cast(*Rhs)); if (Result < 0) { return true; } else if (Result > 0 || !SecondaryCompare) { return false; } else if (SecondarySortMode == EColumnSortMode::Ascending) { return SecondaryCompare(static_cast(*Lhs), static_cast(*Rhs)) < 0; } else { return SecondaryCompare(static_cast(*Lhs), static_cast(*Rhs)) > 0; } }); } else { FileListNodes.Sort([this, &PrimaryCompare, &SecondaryCompare](const TSharedPtr& Lhs, const TSharedPtr& Rhs) { int32 Result = PrimaryCompare(static_cast(*Lhs), static_cast(*Rhs)); if (Result > 0) { return true; } else if (Result < 0 || !SecondaryCompare) { return false; } else if (SecondarySortMode == EColumnSortMode::Ascending) { return SecondaryCompare(static_cast(*Lhs), static_cast(*Rhs)) < 0; } else { return SecondaryCompare(static_cast(*Lhs), static_cast(*Rhs)) > 0; } }); } } void SSourceControlChangelistsWidget::OnChangelistSearchTextChanged(const FText& InFilterText) { ChangelistTextFilter->SetRawFilterText(InFilterText); ChangelistExpandableArea->GetSearchBox()->SetError(ChangelistTextFilter->GetFilterErrorText()); } void SSourceControlChangelistsWidget::OnUncontrolledChangelistSearchTextChanged(const FText& InFilterText) { UncontrolledChangelistTextFilter->SetRawFilterText(InFilterText); UncontrolledChangelistExpandableArea->GetSearchBox()->SetError(UncontrolledChangelistTextFilter->GetFilterErrorText()); } void SSourceControlChangelistsWidget::OnFileSearchTextChanged(const FText& InFilterText) { FileTextFilter->SetRawFilterText(InFilterText); FileSearchBox->SetError(FileTextFilter->GetFilterErrorText()); } void SSourceControlChangelistsWidget::PopulateItemSearchStrings(const IChangelistTreeItem& Item, TArray& OutStrings) { switch (Item.GetTreeItemType()) { case IChangelistTreeItem::Changelist: SChangelistTableRow::PopulateSearchString(static_cast(Item), OutStrings); break; case IChangelistTreeItem::ShelvedChangelist: SShelvedFilesTableRow::PopulateSearchString(static_cast(Item), OutStrings); break; case IChangelistTreeItem::UncontrolledChangelist: SUncontrolledChangelistTableRow::PopulateSearchString(static_cast(Item), OutStrings); break; case IChangelistTreeItem::File: case IChangelistTreeItem::ShelvedFile: SFileTableRow::PopulateSearchString( static_cast(Item), bShowingContentVersePath ? SourceControlFileViewColumn::EPathFlags::ShowingVersePath : SourceControlFileViewColumn::EPathFlags::Default, OutStrings); break; case IChangelistTreeItem::OfflineFile: SOfflineFileTableRow::PopulateSearchString( static_cast(Item), bShowingContentVersePath ? SourceControlFileViewColumn::EPathFlags::ShowingVersePath : SourceControlFileViewColumn::EPathFlags::Default, OutStrings); break; default: checkNoEntry(); } } void SSourceControlChangelistsWidget::OnUnsavedAssetChanged(const FString& /*Filepath*/) { OnRefreshUI(ERefreshFlags::UnsavedAssets); } void SSourceControlChangelistsWidget::OnPackageSaved(const FString& PackageFilename, UPackage* Package, FObjectPostSaveContext ObjectSaveContext) { OutdatedTimestampFiles.Emplace(PackageFilename); // The background task will convert relative to absolute path. } void SSourceControlChangelistsWidget::OnPluginEdited(IPlugin& InPlugin) { // The plugin's Verse path may have changed. Recompute all Verse paths. bShouldRefreshVersePaths = true; } SListView& SSourceControlChangelistsWidget::GetActiveFileListView() const { return *StaticCastSharedPtr>(FileListViewSwitcher->GetActiveWidget()); } #undef LOCTEXT_NAMESPACE