// Copyright Epic Games, Inc. All Rights Reserved. #include "Merge.h" #include "Engine/Blueprint.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SBoxPanel.h" #include "Framework/Docking/TabManager.h" #include "ISourceControlState.h" #include "ISourceControlRevision.h" #include "ISourceControlProvider.h" #include "ISourceControlModule.h" #include "IAssetTypeActions.h" #include "BlueprintMergeData.h" #include "MergeUtils.h" #include "UObject/Package.h" #include "BlueprintEditor.h" #include "SBlueprintMerge.h" #include "Widgets/Docking/SDockTab.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" #define LOCTEXT_NAMESPACE "Merge" const FName MergeToolTabId = FName(TEXT("MergeTool")); static void DisplayErrorMessage( const FText& ErrorMessage ) { FNotificationInfo Info(ErrorMessage); Info.ExpireDuration = 5.0f; FSlateNotificationManager::Get().AddNotification(Info); } static FRevisionInfo GetRevisionInfo(ISourceControlRevision const& FromRevision) { FRevisionInfo Ret = { FromRevision.GetRevision(), FromRevision.GetCheckInIdentifier(), FromRevision.GetDate() }; return Ret; } static const UObject* LoadHeadRev(const FString& PackageName, const FString& AssetName, const ISourceControlState& SourceControlState, FRevisionInfo& OutRevInfo) { // HistoryItem(0) is apparently the head revision: TSharedPtr Revision = SourceControlState.GetHistoryItem(0); check(Revision.IsValid()); OutRevInfo = GetRevisionInfo(*Revision); return FMergeToolUtils::LoadRevision(AssetName, *Revision); } static const UObject* LoadBaseRev(const FString& PackageName, const FString& AssetName, const ISourceControlState& SourceControlState, FRevisionInfo& OutRevInfo) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); const ISourceControlState::FResolveInfo ResolveInfo = SourceControlState.GetResolveInfo(); if (ResolveInfo.IsValid()) { const FSourceControlStatePtr BaseBranch = SourceControlProvider.GetState(ResolveInfo.BaseFile, EStateCacheUsage::Use); if (BaseBranch.IsValid()) { const TSharedPtr Revision = BaseBranch->FindHistoryRevision(ResolveInfo.BaseRevision); if (Revision.IsValid()) { OutRevInfo = GetRevisionInfo(*Revision); return FMergeToolUtils::LoadRevision(AssetName, *Revision); } } } OutRevInfo = FRevisionInfo::InvalidRevision(); return nullptr; } static TSharedPtr GenerateMergeTabContents(TSharedRef Editor, const UBlueprint* BaseBlueprint, const FRevisionInfo& BaseRevInfo, const UBlueprint* RemoteBlueprint, const FRevisionInfo& RemoteRevInfo, const UBlueprint* LocalBlueprint, const FOnMergeResolved& MereResolutionCallback) { bool bForceAssetPicker = false; if (BaseBlueprint == nullptr) { BaseBlueprint = LocalBlueprint; bForceAssetPicker = true; } if (RemoteBlueprint == nullptr) { RemoteBlueprint = LocalBlueprint; bForceAssetPicker = true; } FBlueprintMergeData Data(Editor , LocalBlueprint , BaseBlueprint , BaseRevInfo , RemoteBlueprint , RemoteRevInfo); return SNew(SBlueprintMerge, Data) .bForcePickAssets(bForceAssetPicker) .OnMergeResolved(MereResolutionCallback); } class FMerge : public IMerge { /** IModuleInterface implementation */ virtual void StartupModule() override; virtual void ShutdownModule() override; virtual TSharedPtr GenerateMergeWidget(const UBlueprint& Object, TSharedRef Editor) override; virtual TSharedPtr GenerateMergeWidget(const UBlueprint* BaseAsset, const UBlueprint* RemoteAsset, const UBlueprint* LocalAsset, const FOnMergeResolved& MergeResolutionCallback, TSharedRef Editor) override; virtual bool PendingMerge(const UBlueprint& BlueprintObj) const override; //virtual FOnMergeResolved& OnMergeResolved() const override; // Simplest to only allow one merge operation at a time, we could easily make this a map of Blueprint=>MergeTab // but doing so will complicate the tab management TWeakPtr ActiveTab; }; IMPLEMENT_MODULE( FMerge, Merge ) void FMerge::StartupModule() { // This code will execute after your module is loaded into memory (but after global variables are initialized, of course.) // Registering a nomad spawner that spawns an empty dock tab on purpose - allows us to call TryInvokeTab() using our TabId later and set the content. (see GenerateMergeWidget()) FGlobalTabmanager::Get()->RegisterNomadTabSpawner(MergeToolTabId, FOnSpawnTab::CreateStatic([] (const FSpawnTabArgs&) { return SNew(SDockTab); })) .SetDisplayName(NSLOCTEXT("MergeTool", "TabTitle", "Merge Tool")) .SetTooltipText(NSLOCTEXT("MergeTool", "TooltipText", "Used to display several versions of a blueprint that need to be merged into a single version.")) .SetAutoGenerateMenuEntry(false); } void FMerge::ShutdownModule() { // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, // we call this function before unloading the module. FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(MergeToolTabId); } TSharedPtr FMerge::GenerateMergeWidget(const UBlueprint& Object, TSharedRef Editor) { TSharedPtr ActiveTabPtr = ActiveTab.Pin(); if( ActiveTabPtr.IsValid() ) { // just bring the tab to the foreground: TSharedPtr CurrentTab = FGlobalTabmanager::Get()->TryInvokeTab(MergeToolTabId); if (CurrentTab.IsValid()) { check(CurrentTab == ActiveTabPtr); return ActiveTabPtr; } else { return nullptr; } } // merge the local asset with the depot, SCC provides us with the last common revision as // a basis for the merge: TSharedPtr Contents; if (!PendingMerge(Object)) { // this should load up the merge-tool, with an asset picker, where they // can pick the asset/revisions to merge against Contents = GenerateMergeTabContents(Editor, nullptr, FRevisionInfo::InvalidRevision(), nullptr, FRevisionInfo::InvalidRevision(), &Object, FOnMergeResolved()); } else { // @todo DO: this will probably need to be async.. pulling down some old versions of assets: const FString& PackageName = Object.GetOutermost()->GetName(); const FString& AssetName = Object.GetName(); FSourceControlStatePtr SourceControlState = FMergeToolUtils::GetSourceControlState(PackageName); if (!SourceControlState.IsValid()) { DisplayErrorMessage( FText::Format( LOCTEXT("MergeFailedNoSourceControl", "Aborted Load of {0} from {1} because the revision control state was invalidated") , FText::FromString(AssetName) , FText::FromString(PackageName) ) ); Contents = SNew(SHorizontalBox); } else { ISourceControlState const& SourceControlStateRef = *SourceControlState; FRevisionInfo CurrentRevInfo = FRevisionInfo::InvalidRevision(); const UBlueprint* RemoteBlueprint = Cast< UBlueprint >(LoadHeadRev(PackageName, AssetName, SourceControlStateRef, CurrentRevInfo)); FRevisionInfo BaseRevInfo = FRevisionInfo::InvalidRevision(); const UBlueprint* BaseBlueprint = Cast< UBlueprint >(LoadBaseRev(PackageName, AssetName, SourceControlStateRef, BaseRevInfo)); Contents = GenerateMergeTabContents(Editor, BaseBlueprint, BaseRevInfo, RemoteBlueprint, CurrentRevInfo, &Object, FOnMergeResolved()); } } TSharedPtr Tab = FGlobalTabmanager::Get()->TryInvokeTab(MergeToolTabId); if (Tab.IsValid()) { Tab->SetContent(Contents.ToSharedRef()); ActiveTab = Tab; } return Tab; } TSharedPtr FMerge::GenerateMergeWidget(const UBlueprint* BaseBlueprint, const UBlueprint* RemoteBlueprint, const UBlueprint* LocalBlueprint, const FOnMergeResolved& MergeResolutionCallback, TSharedRef Editor) { if (ActiveTab.IsValid()) { TSharedPtr ActiveTabPtr = ActiveTab.Pin(); // just bring the tab to the foreground: TSharedPtr CurrentTab = FGlobalTabmanager::Get()->TryInvokeTab(MergeToolTabId); if (CurrentTab.IsValid()) { check(CurrentTab == ActiveTabPtr); return ActiveTabPtr; } else { return nullptr; } } // @TODO: pipe revision info through TSharedPtr TabContents = GenerateMergeTabContents(Editor, BaseBlueprint, FRevisionInfo::InvalidRevision(), RemoteBlueprint, FRevisionInfo::InvalidRevision(), LocalBlueprint, MergeResolutionCallback); TSharedPtr Tab = FGlobalTabmanager::Get()->TryInvokeTab(MergeToolTabId); if (Tab.IsValid()) { Tab->SetContent(TabContents.ToSharedRef()); ActiveTab = Tab; } return Tab; } bool FMerge::PendingMerge(const UBlueprint& BlueprintObj) const { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); bool bPendingMerge = false; if( SourceControlProvider.IsEnabled() ) { FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(BlueprintObj.GetOutermost(), EStateCacheUsage::Use); bPendingMerge = SourceControlState.IsValid() && SourceControlState->IsConflicted(); } return bPendingMerge; } #undef LOCTEXT_NAMESPACE