// Copyright Epic Games, Inc. All Rights Reserved. #include "SourceControlMenuHelpers.h" #include "SourceControlOperations.h" #include "ISourceControlProvider.h" #include "ISourceControlModule.h" #include "ISourceControlWindowsModule.h" #include "SourceControlWindows.h" #include "UnsavedAssetsTrackerModule.h" #include "FileHelpers.h" #include "Logging/MessageLog.h" #include "ToolMenuContext.h" #include "ToolMenus.h" #include "Styling/AppStyle.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Input/SButton.h" #include "Widgets/Images/SImage.h" #include "Widgets/Layout/SSeparator.h" #include "Widgets/Images/SLayeredImage.h" #include "PackageTools.h" #include "Editor.h" #include "EditorModeManager.h" #include "Misc/ConfigCacheIni.h" #include "Misc/MessageDialog.h" #include "RevisionControlStyle/RevisionControlStyle.h" #include "Bookmarks/BookmarkScoped.h" #include "Styling/StyleColors.h" #include "HAL/IConsoleManager.h" #include "SSourceControlControls.h" #define LOCTEXT_NAMESPACE "SourceControlCommands" TSharedRef FSourceControlCommands::ActionList(new FUICommandList()); FSourceControlCommands::FSourceControlCommands() : TCommands ( "SourceControl", NSLOCTEXT("Contexts", "SourceControl", "Revision Control"), "LevelEditor", FAppStyle::GetAppStyleSetName() ) {} /** * Initialize commands */ void FSourceControlCommands::RegisterCommands() { UI_COMMAND(ConnectToSourceControl, "Connect to Revision Control...", "Connect to a revision control system for tracking changes to your content and levels.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(ChangeSourceControlSettings, "Change Revision Control Settings...", "Opens a dialog to change revision control settings.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(ViewChangelists, "View Changes", "Opens a dialog displaying current changes.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(SubmitContent, "Submit Content", "Opens a dialog with check in options for content and levels.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(ViewSnapshotHistory, "Open Snapshot History", "See all the changes that have been made to this project over time.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(CheckOutModifiedFiles, "Check Out Modified Files", "Opens a dialog to check out any assets which have been modified.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(RevertAll, "Revert All Files", "Opens a dialog to revert any assets which have been modified.", EUserInterfaceActionType::Button, FInputChord()); ActionList->MapAction( ConnectToSourceControl, FExecuteAction::CreateStatic(&FSourceControlCommands::ConnectToSourceControl_Clicked) ); ActionList->MapAction( ChangeSourceControlSettings, FExecuteAction::CreateStatic(&FSourceControlCommands::ConnectToSourceControl_Clicked) ); ActionList->MapAction( SubmitContent, FExecuteAction::CreateStatic(&FSourceControlCommands::SubmitContent_Clicked), FCanExecuteAction::CreateStatic(&FSourceControlCommands::SubmitContent_CanExecute), FIsActionChecked::CreateLambda([]() { return false; }), FIsActionButtonVisible::CreateStatic(&FSourceControlCommands::SubmitContent_IsVisible) ); ActionList->MapAction( ViewChangelists, FExecuteAction::CreateStatic(&FSourceControlCommands::ViewChangelists_Clicked), FCanExecuteAction::CreateStatic(&FSourceControlCommands::ViewChangelists_CanExecute), FIsActionChecked::CreateLambda([]() { return false; }), FIsActionButtonVisible::CreateStatic(&FSourceControlCommands::ViewChangelists_IsVisible) ); ActionList->MapAction( ViewSnapshotHistory, FExecuteAction::CreateStatic(&FSourceControlCommands::ViewSnapshotHistory_Clicked), FCanExecuteAction::CreateStatic(&FSourceControlCommands::ViewSnapshotHistory_CanExecute), FIsActionChecked::CreateLambda([]() { return false; }), FIsActionButtonVisible::CreateStatic(&FSourceControlCommands::ViewSnapshotHistory_IsVisible) ); ActionList->MapAction( CheckOutModifiedFiles, FExecuteAction::CreateStatic(&FSourceControlCommands::CheckOutModifiedFiles_Clicked), FCanExecuteAction::CreateStatic(&FSourceControlCommands::CheckOutModifiedFiles_CanExecute), FIsActionChecked::CreateLambda([]() { return false; }), FIsActionButtonVisible::CreateStatic(&FSourceControlCommands::CheckOutModifiedFiles_IsVisible) ); ActionList->MapAction( RevertAll, FExecuteAction::CreateStatic(&FSourceControlCommands::RevertAllModifiedFiles_Clicked), FCanExecuteAction::CreateStatic(&FSourceControlCommands::RevertAllModifiedFiles_CanExecute) ); } void FSourceControlCommands::ConnectToSourceControl_Clicked() { // Show login window regardless of current status - its useful as a shortcut to change settings. ISourceControlModule& SourceControlModule = ISourceControlModule::Get(); SourceControlModule.ShowLoginDialog(FSourceControlLoginClosed(), ELoginWindowMode::Modeless, EOnLoginWindowStartup::PreserveProvider); } bool FSourceControlCommands::ViewChangelists_CanExecute() { return ISourceControlWindowsModule::Get().CanShowChangelistsTab(); } bool FSourceControlCommands::ViewChangelists_IsVisible() { return ISourceControlModule::Get().GetProvider().UsesChangelists() || ISourceControlModule::Get().GetProvider().UsesUncontrolledChangelists(); } bool FSourceControlCommands::SubmitContent_CanExecute() { return FSourceControlWindows::CanChoosePackagesToCheckIn(); } bool FSourceControlCommands::SubmitContent_IsVisible() { // Access to the legacy 'Submit Files' dialog. Git / Subversion require this. if (ISourceControlModule::Get().GetProvider().UsesChangelists()) { return false; } if (ISourceControlModule::Get().GetProvider().GetName() == TEXT("Unreal Revision Control")) { return false; } return true; } bool FSourceControlCommands::ViewSnapshotHistory_CanExecute() { return ISourceControlWindowsModule::Get().CanShowSnapshotHistoryTab(); } bool FSourceControlCommands::ViewSnapshotHistory_IsVisible() { return ISourceControlModule::Get().GetProvider().GetName() == TEXT("Unreal Revision Control"); } void FSourceControlCommands::ViewChangelists_Clicked() { ISourceControlWindowsModule::Get().ShowChangelistsTab(); } void FSourceControlCommands::SubmitContent_Clicked() { FSourceControlWindows::ChoosePackagesToCheckIn(); } void FSourceControlCommands::ViewSnapshotHistory_Clicked() { ISourceControlWindowsModule::Get().ShowSnapshotHistoryTab(); } bool FSourceControlCommands::CheckOutModifiedFiles_CanExecute() { ISourceControlModule& SourceControlModule = ISourceControlModule::Get(); if (ISourceControlModule::Get().IsEnabled() && ISourceControlModule::Get().GetProvider().IsAvailable()) { TArray PackagesToSave; FEditorFileUtils::GetDirtyWorldPackages(PackagesToSave); FEditorFileUtils::GetDirtyContentPackages(PackagesToSave); return PackagesToSave.Num() > 0; } return false; } bool FSourceControlCommands::CheckOutModifiedFiles_IsVisible() { return ISourceControlModule::Get().GetProvider().UsesCheckout(); } void FSourceControlCommands::CheckOutModifiedFiles_Clicked() { TArray PackagesToSave; FEditorFileUtils::GetDirtyWorldPackages(PackagesToSave); FEditorFileUtils::GetDirtyContentPackages(PackagesToSave); FEditorFileUtils::FPromptForCheckoutAndSaveParams SaveParams; SaveParams.bCheckDirty = true; SaveParams.bPromptToSave = false; SaveParams.bIsExplicitSave = true; FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, SaveParams); } bool FSourceControlCommands::RevertAllModifiedFiles_CanExecute() { // If CHECK-IN CHANGES button is active, the REVERT ALL option should be active. if (FUnsavedAssetsTrackerModule::Get().GetUnsavedAssetNum() > 0) { return true; } if (FSourceControlWindows::CanChoosePackagesToCheckIn()) { return true; } return false; } void FSourceControlCommands::RevertAllModifiedFiles_Clicked() { FText Message = LOCTEXT("RevertAllModifiedFiles", "Are you sure you want to revert all local changes? By proceeding, your local changes will be discarded, and the state of your last synced snapshot will be restored."); FText Title = LOCTEXT("RevertAllModifiedFiles_Title", "Revert all local changes"); if (FMessageDialog::Open(EAppMsgType::YesNo, EAppReturnType::No, Message, Title) == EAppReturnType::Yes) { const bool bPromptUserToSave = false; const bool bSaveMapPackages = true; const bool bSaveContentPackages = true; const bool bFastSave = true; const bool bNotifyNoPackagesSaved = false; const bool bCanBeDeclined = false; FEditorFileUtils::SaveDirtyPackages(bPromptUserToSave, bSaveMapPackages, bSaveContentPackages, bFastSave, bNotifyNoPackagesSaved, bCanBeDeclined); FBookmarkScoped BookmarkScoped; FSourceControlWindows::RevertAllChangesAndReloadWorld(); } } FSourceControlMenuHelpers::EQueryState FSourceControlMenuHelpers::QueryState = FSourceControlMenuHelpers::EQueryState::NotQueried; void FSourceControlMenuHelpers::CheckSourceControlStatus() { ISourceControlModule& SourceControlModule = ISourceControlModule::Get(); if (SourceControlModule.IsEnabled()) { TSharedRef Operation = ISourceControlOperation::Create(); // Disable logging of info messages to prevent dumping 100+ rows of client info to the log every time an asset editor is opened. Operation->SetEnableInfoLogging(false); SourceControlModule.GetProvider().Execute( Operation, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateStatic(&FSourceControlMenuHelpers::OnSourceControlOperationComplete) ); QueryState = EQueryState::Querying; } } void FSourceControlMenuHelpers::OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult) { QueryState = EQueryState::Queried; } TSharedRef FSourceControlMenuHelpers::GenerateSourceControlMenuContent() { // NOTE: This menu should be in sync with MainMenu's SourceControl menu. UToolMenu* SourceControlMenu = UToolMenus::Get()->RegisterMenu("StatusBar.ToolBar.SourceControl", NAME_None, EMultiBoxType::Menu, false); FToolMenuSection& Section = SourceControlMenu->AddSection("SourceControlActions", LOCTEXT("SourceControlMenuHeadingActions", "Actions")); Section.AddMenuEntry( FSourceControlCommands::Get().ViewChangelists, TAttribute(), TAttribute(), FSlateIcon(FRevisionControlStyleManager::GetStyleSetName(), "RevisionControl.ChangelistsTab") ); Section.AddMenuEntry( FSourceControlCommands::Get().SubmitContent, TAttribute(), TAttribute(), FSlateIcon(FRevisionControlStyleManager::GetStyleSetName(), "RevisionControl.Actions.Submit") ); Section.AddMenuEntry( FSourceControlCommands::Get().ViewSnapshotHistory, TAttribute(), TAttribute(), FSlateIcon(FRevisionControlStyleManager::GetStyleSetName(), "RevisionControl.Actions.Rewind") ); Section.AddMenuEntry( FSourceControlCommands::Get().CheckOutModifiedFiles, TAttribute(), TAttribute(), FSlateIcon(FRevisionControlStyleManager::GetStyleSetName(), "RevisionControl.Actions.CheckOut") ); Section.AddDynamicEntry("ConnectToSourceControl", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) { ISourceControlModule& SourceControlModule = ISourceControlModule::Get(); if (ISourceControlModule::Get().IsEnabled() && ISourceControlModule::Get().GetProvider().IsAvailable()) { InSection.AddMenuEntry( FSourceControlCommands::Get().ChangeSourceControlSettings, TAttribute(), TAttribute(), FSlateIcon(FRevisionControlStyleManager::GetStyleSetName(), "RevisionControl.Actions.ChangeSettings") ); } else { InSection.AddMenuEntry( FSourceControlCommands::Get().ConnectToSourceControl, TAttribute(), TAttribute(), FSlateIcon(FRevisionControlStyleManager::GetStyleSetName(), "RevisionControl.Actions.Connect") ); } })); return UToolMenus::Get()->GenerateWidget("StatusBar.ToolBar.SourceControl", FToolMenuContext(FSourceControlCommands::ActionList)); } TSharedRef FSourceControlMenuHelpers::GenerateCheckInComboButtonContent() { UToolMenu* SourceControlMenu = UToolMenus::Get()->RegisterMenu("StatusBar.ToolBar.SourceControl.CheckInCombo", NAME_None, EMultiBoxType::Menu, false); FToolMenuSection& Section = SourceControlMenu->AddSection("SourceControlComboActions", LOCTEXT("SourceControlComboMenuHeadingActions", "Actions")); Section.AddMenuEntry( FSourceControlCommands::Get().RevertAll, TAttribute(), TAttribute(), FSlateIcon(FRevisionControlStyleManager::Get().GetStyleSetName(), "RevisionControl.Actions.Revert") ); return UToolMenus::Get()->GenerateWidget("StatusBar.ToolBar.SourceControl.CheckInCombo", FToolMenuContext(FSourceControlCommands::ActionList)); } FText FSourceControlMenuHelpers::GetSourceControlStatusText() { if (QueryState == EQueryState::Querying) { return LOCTEXT("SourceControlStatus_Querying", "Contacting Server...."); } else { ISourceControlModule& SourceControlModule = ISourceControlModule::Get(); if (SourceControlModule.IsEnabled()) { if (!SourceControlModule.GetProvider().IsAvailable()) { return LOCTEXT("SourceControlStatus_Error_ServerUnavailable", "Server Unavailable"); } else { return LOCTEXT("SourceControlStatus_Available", "Revision Control"); } } else { return LOCTEXT("SourceControlStatus_Error_Off", "Revision Control"); // Relies on the icon on the status bar widget to know if the source control is on or off. } } } FText FSourceControlMenuHelpers::GetSourceControlTooltip() { if (QueryState == EQueryState::Querying) { return LOCTEXT("SourceControlUnknown", "Revision control status is unknown"); } else { return ISourceControlModule::Get().GetProvider().GetStatusText(); } } const FSlateBrush* FSourceControlMenuHelpers::GetSourceControlIconBadge() { if (QueryState == EQueryState::Querying) { return nullptr; } else { ISourceControlModule& SourceControlModule = ISourceControlModule::Get(); if (SourceControlModule.IsEnabled()) { if (!SourceControlModule.GetProvider().IsAvailable()) { static const FSlateBrush* ErrorBrush = FRevisionControlStyleManager::Get().GetBrush("RevisionControl.Icon.WarningBadge"); return ErrorBrush; } else { static const FSlateBrush* OnBrush = FRevisionControlStyleManager::Get().GetBrush("RevisionControl.Icon.ConnectedBadge"); return OnBrush; } } else { static const FSlateBrush* OffBrush = nullptr; return OffBrush; } } } TSharedRef FSourceControlMenuHelpers::MakeSourceControlStatusWidget() { TSharedRef SourceControlIcon = SNew(SLayeredImage) .ColorAndOpacity(FSlateColor::UseForeground()) .Image(FRevisionControlStyleManager::Get().GetBrush("RevisionControl.Icon")); SourceControlIcon->AddLayer(TAttribute::CreateStatic(&FSourceControlMenuHelpers::GetSourceControlIconBadge)); return SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() [ // NOTE: The unsaved can have its own menu extension in the status bar to decouple it from source control, but putting all the buttons in the right order // on the status bar is not deterministic, you need to know the name of the menu that is before or after and the menu is dynamic. Having it here // ensure its position with respect to the source control button. FUnsavedAssetsTrackerModule::Get().MakeUnsavedAssetsStatusBarWidget() ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4.0f, -5.0f) // Intentional negative padding to make the separator cover the whole status bar vertically [ SNew(SSeparator) .Thickness(2.0f) .Orientation(EOrientation::Orient_Vertical) ] +SHorizontalBox::Slot() .Padding(0.f) [ SNew(SSourceControlControls) .OnGenerateKebabMenu_Static(&FSourceControlMenuHelpers::GenerateCheckInComboButtonContent) ] + SHorizontalBox::Slot() // Source Control Menu .VAlign(VAlign_Center) .AutoWidth() [ SNew(SComboButton) .ContentPadding(FMargin(6.0f, 0.0f)) .ToolTipText_Static(&FSourceControlMenuHelpers::GetSourceControlTooltip) .MenuPlacement(MenuPlacement_AboveAnchor) .ComboButtonStyle(&FAppStyle::Get().GetWidgetStyle("SimpleComboButton")) .ButtonContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .HAlign(HAlign_Center) [ SourceControlIcon ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(FMargin(5, 0, 0, 0)) [ SNew(STextBlock) .TextStyle(&FAppStyle::Get().GetWidgetStyle("NormalText")) .Text_Static(&FSourceControlMenuHelpers::GetSourceControlStatusText) ] ] .OnGetMenuContent(FOnGetContent::CreateStatic(&FSourceControlMenuHelpers::GenerateSourceControlMenuContent)) ]; } #undef LOCTEXT_NAMESPACE