// Copyright Epic Games, Inc. All Rights Reserved. #include "PackageRestore.h" #include "HAL/FileManager.h" #include "Misc/Paths.h" #include "Layout/Margin.h" #include "Input/Reply.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SCompoundWidget.h" #include "Widgets/SBoxPanel.h" #include "Styling/SlateTypes.h" #include "Widgets/SWindow.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Images/SImage.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Input/SButton.h" #include "Widgets/Views/STableViewBase.h" #include "Widgets/Views/STableRow.h" #include "Widgets/Views/SListView.h" #include "Widgets/Input/SCheckBox.h" #include "EditorStyleSet.h" #include "Editor.h" #include "Misc/MessageDialog.h" #include "PackageTools.h" #include "AutoSaveUtils.h" #define LOCTEXT_NAMESPACE "PackageRestore" namespace PackageRestore { /** An item in the SPackageRestoreDialog package list */ class FPackageRestoreItem: public TSharedFromThis { public: FPackageRestoreItem(const FString& InPackageName, const FString& InPackageFilename, const FString& InAutoSaveFilename, const bool bInIsExistingPackage) : PackageName(InPackageName), PackageFilename(InPackageFilename), AutoSaveFilename(InAutoSaveFilename), bIsExistingPackage(bInIsExistingPackage), State(ECheckBoxState::Unchecked) { } /** @return The package name for this item */ const FString& GetPackageName() const { return PackageName; } /** @return The package filename for this item */ const FString& GetPackageFilename() const { return PackageFilename; } /** @return The package auto-save filename for this item */ const FString& GetAutoSaveFilename() const { return AutoSaveFilename; } /** @return True if this item is to replace an existing package, or false if it is to add a new package */ bool IsExistingPackage() const { return bIsExistingPackage; } /** @return The state of this item (checked, unchecked) */ ECheckBoxState GetState() const { return State; } /** Set the state of this item (checked, unchecked) */ void SetState(const ECheckBoxState InState) { State = InState; } /** @return The tooltip text for this item */ FText GetToolTip() const { FFormatNamedArguments Args; Args.Add(TEXT("PackageName"), LOCTEXT("PackageName", "Package Name")); Args.Add(TEXT("PackageFile"), LOCTEXT("PackageFile", "Package File")); Args.Add(TEXT("AutoSaveFile"), LOCTEXT("AutoSaveFile", "Autosave File")); Args.Add(TEXT("PackageNameStr"), FText::FromString(PackageName)); Args.Add(TEXT("PackageFileStr"), FText::FromString(PackageFilename)); Args.Add(TEXT("AutoSaveFileStr"), FText::FromString(AutoSaveFilename)); return FText::Format(FText::FromString("{PackageName}: {PackageNameStr}\n\n{PackageFile}: {PackageFileStr}\n\n{AutoSaveFile}: {AutoSaveFileStr}"), Args); } /** @return Process a request to navigate to the package location */ FReply OnExploreToPackage(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) const { const FString AbsoluteFilename = FPaths::ConvertRelativePathToFull(PackageFilename); const FString AbsolutePath = FPaths::GetPath(AbsoluteFilename); FPlatformProcess::ExploreFolder(*AbsolutePath); return FReply::Handled(); } /** @return Process a request to navigate to the auto-save location */ FReply OnExploreToAutoSave(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) const { const FString AbsoluteFilename = FPaths::ConvertRelativePathToFull(AutoSaveFilename); const FString AbsolutePath = FPaths::GetPath(AbsoluteFilename); FPlatformProcess::ExploreFolder(*AbsolutePath); return FReply::Handled(); } private: FString PackageName; FString PackageFilename; FString AutoSaveFilename; bool bIsExistingPackage; ECheckBoxState State; }; typedef TSharedPtr FPackageRestoreItemPtr; typedef TArray FPackageRestoreItems; /** Dialog for letting the user choose which packages they want to restore */ class SPackageRestoreDialog: public SCompoundWidget { public: SLATE_BEGIN_ARGS(SPackageRestoreDialog) {} /** Information about which packages to offer restoration for */ SLATE_ATTRIBUTE(FPackageRestoreItems*, PackageRestoreItems) SLATE_END_ARGS() /** * Construct this widget * * @param InArgs The declaration data for this widget */ void Construct(const FArguments& InArgs) { PackageRestoreItems = InArgs._PackageRestoreItems.Get(); ReturnCode = false; this->ChildSlot [SNew(SBorder) .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) [SNew(SVerticalBox) + SVerticalBox::Slot().Padding(10).AutoHeight()[SNew(STextBlock).Text(LOCTEXT("RestoreInfo", "Unreal Editor detected that it did not shut-down cleanly and that the following packages have auto-saves associated with them.\nWould you like to restore from these auto-saves?")).AutoWrapText(true)] + SVerticalBox::Slot().FillHeight(1)[SNew(SBorder)[SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight()[SNew(SBorder).BorderImage(FEditorStyle::GetBrush(TEXT("PackageDialog.ListHeader")))[SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).HAlign(HAlign_Center)[SNew(SCheckBox).IsChecked(this, &SPackageRestoreDialog::GetToggleSelectedState).OnCheckStateChanged(this, &SPackageRestoreDialog::OnToggleSelectedCheckBox)] + SHorizontalBox::Slot().Padding(FMargin(2, 0, 0, 0)).VAlign(VAlign_Center).HAlign(HAlign_Left).FillWidth(1)[SNew(STextBlock).Text(LOCTEXT("PackageName", "Package Name"))] + SHorizontalBox::Slot().Padding(FMargin(4, 0, 0, 0)).VAlign(VAlign_Center).HAlign(HAlign_Left).FillWidth(1)[SNew(STextBlock).Text(LOCTEXT("PackageFile", "Package File"))] + SHorizontalBox::Slot().Padding(FMargin(4, 0, 0, 0)).VAlign(VAlign_Center).HAlign(HAlign_Left).FillWidth(1)[SNew(STextBlock).Text(LOCTEXT("AutoSaveFile", "Autosave File"))]]] + SVerticalBox::Slot()[SAssignNew(ItemListView, SListView).ListItemsSource(PackageRestoreItems).OnGenerateRow(this, &SPackageRestoreDialog::MakePackageRestoreListItemWidget).ItemHeight(20)]]] + SVerticalBox::Slot().AutoHeight().Padding(2).HAlign(HAlign_Right).VAlign(VAlign_Bottom)[SNew(SHorizontalBox) + SHorizontalBox::Slot().Padding(2).AutoWidth()[SNew(SButton).Text(LOCTEXT("RestoreSelectedPackages", "Restore Selected")).OnClicked(this, &SPackageRestoreDialog::OnRestoreSelectedButtonClicked).IsEnabled(this, &SPackageRestoreDialog::IsRestoreSelectedButtonEnabled)] + SHorizontalBox::Slot().Padding(2).AutoWidth()[SNew(SButton).Text(LOCTEXT("SkipRestorePackages", "Skip Restore")).OnClicked(this, &SPackageRestoreDialog::OnSkipRestoreButtonClicked)]]]]; } /** * Set the window which owns us (we'll close it when we're finished) */ void SetWindow(TSharedRef InWindow) { ParentWindowPtr = InWindow; } /** * Makes the widget for the checkbox items in the list view */ TSharedRef MakePackageRestoreListItemWidget(FPackageRestoreItemPtr Item, const TSharedRef& OwnerTable) { check(Item.IsValid()); const FSlateBrush* FolderOpenBrush = FEditorStyle::GetBrush("PackageRestore.FolderOpen"); return SNew(STableRow, OwnerTable) .Padding(FMargin(2, 0)) [SNew(SHorizontalBox) .ToolTipText(Item->GetToolTip()) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .HAlign(HAlign_Center) [SNew(SCheckBox) .IsChecked(Item.Get(), &FPackageRestoreItem::GetState) .OnCheckStateChanged(Item.Get(), &FPackageRestoreItem::SetState)] + SHorizontalBox::Slot() .Padding(FMargin(2, 0, 0, 0)) .VAlign(VAlign_Center) .HAlign(HAlign_Left) .FillWidth(1) [SNew(STextBlock) .Text(FText::FromString(Item->GetPackageName()))] + SHorizontalBox::Slot() .Padding(FMargin(4, 0, 0, 0)) .VAlign(VAlign_Center) .HAlign(HAlign_Left) .FillWidth(1) [SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1)[SNew(STextBlock).Text(FText::FromString(Item->GetPackageFilename()))] + SHorizontalBox::Slot().Padding(FMargin(2, 0, 0, 0)).AutoWidth()[SNew(SImage).Image(FolderOpenBrush).OnMouseButtonDown(Item.Get(), &FPackageRestoreItem::OnExploreToPackage)]] + SHorizontalBox::Slot() .Padding(FMargin(4, 0, 0, 0)) .VAlign(VAlign_Center) .HAlign(HAlign_Left) .FillWidth(1) [SNew(SHorizontalBox) + SHorizontalBox::Slot().FillWidth(1)[SNew(STextBlock).Text(FText::FromString(Item->GetAutoSaveFilename()))] + SHorizontalBox::Slot().Padding(FMargin(2, 0, 0, 0)).AutoWidth()[SNew(SImage).Image(FolderOpenBrush).OnMouseButtonDown(Item.Get(), &FPackageRestoreItem::OnExploreToAutoSave)]]]; } /** * @return the desired toggle state for the ToggleSelectedCheckBox. * Returns Unchecked, unless all of the selected packages are Checked. */ ECheckBoxState GetToggleSelectedState() const { // Default to a Checked state ECheckBoxState PendingState = ECheckBoxState::Checked; for (auto It = PackageRestoreItems->CreateConstIterator(); It; ++It) { const FPackageRestoreItemPtr& ListItem = *It; if (ListItem->GetState() == ECheckBoxState::Unchecked) { // If any package in the selection is Unchecked, then represent the entire set of highlighted packages as Unchecked, // so that the first (user) toggle of ToggleSelectedCheckBox consistently Checks all highlighted packages PendingState = ECheckBoxState::Unchecked; } } return PendingState; } /** * Toggles the highlighted packages. * If no packages are explicitly highlighted, toggles all packages in the list. */ void OnToggleSelectedCheckBox(ECheckBoxState InNewState) { for (auto It = PackageRestoreItems->CreateIterator(); It; ++It) { FPackageRestoreItemPtr& ListItem = *It; ListItem->SetState(InNewState); } ItemListView->RequestListRefresh(); } /** * Check to see if the "Restore Selected" button should be enabled */ bool IsRestoreSelectedButtonEnabled() const { // Enabled if anything is selected for (auto It = PackageRestoreItems->CreateConstIterator(); It; ++It) { const FPackageRestoreItemPtr& ListItem = *It; if (ListItem->GetState() == ECheckBoxState::Checked) { return true; } } return false; } /** * Called when the "Restore Selected" button is clicked */ FReply OnRestoreSelectedButtonClicked() { ReturnCode = true; if (ParentWindowPtr.IsValid()) { TSharedPtr ParentWindowPin = ParentWindowPtr.Pin(); ParentWindowPin->RequestDestroyWindow(); } return FReply::Handled(); } /** * Called when the "Skip Restore" button is clicked */ FReply OnSkipRestoreButtonClicked() { if (ParentWindowPtr.IsValid()) { TSharedPtr ParentWindowPin = ParentWindowPtr.Pin(); ParentWindowPin->RequestDestroyWindow(); } return FReply::Handled(); } /** * Get the return code for this dlg, as well as some useful information about what was selected * * @param SelectedPackageItems Array to fill with the list items the user wants to restore * * @return true if we should perform an import, false if the user cancelled */ bool GetReturnType(FPackageRestoreItems& SelectedPackageItems) const { SelectedPackageItems.Empty(); SelectedPackageItems.Reserve(PackageRestoreItems->Num()); // Get the list of packages selected to be restored for (auto It = PackageRestoreItems->CreateConstIterator(); It; ++It) { const FPackageRestoreItemPtr& ListItem = *It; if (ListItem->GetState() == ECheckBoxState::Checked) { SelectedPackageItems.Add(ListItem); } } return ReturnCode; } private: FPackageRestoreItems* PackageRestoreItems; TWeakPtr ParentWindowPtr; TSharedPtr> ItemListView; bool ReturnCode; }; void UnloadPackagesBeforeRestore(const FPackageRestoreItems& SelectedPackageItems, FPackageRestoreItems& OutContentPackagesToReload, FPackageRestoreItemPtr& OutWorldPackageToReload) { // Get the package for the currently loaded world; if we need to restore this package then we also need to unload the current world UPackage* const CurrentWorldPackage = CastChecked(GWorld->GetOuter()); // Work out a list of content packages that need unloading, also work out if we need to unload the current world TArray PackagesToUnload; FPackageRestoreItemPtr CurrentWorldRestoreItem; for (auto It = SelectedPackageItems.CreateConstIterator(); It; ++It) { const FPackageRestoreItemPtr& RestoreItem = *It; if (!RestoreItem->IsExistingPackage()) { continue; } UPackage* const Package = FindPackage(nullptr, *RestoreItem->GetPackageName()); if (Package) { const bool bIsContentPackage = RestoreItem->GetPackageFilename().EndsWith(FPackageName::GetAssetPackageExtension()); if (bIsContentPackage) { // Add this package to the list to be reloaded once we've restored everything PackagesToUnload.Add(Package); OutContentPackagesToReload.Add(RestoreItem); } else if (Package == CurrentWorldPackage) { // If this is the current world, we also need to unload it CurrentWorldRestoreItem = RestoreItem; } } } if (CurrentWorldRestoreItem.IsValid()) { // Replace the current world with an empty world (this may fail) GEditor->CreateNewMapForEditing(); // See if our world package has been unloaded UPackage* const EmptyWorldPackage = CastChecked(GWorld->GetOuter()); if (CurrentWorldPackage != EmptyWorldPackage) { OutWorldPackageToReload = CurrentWorldRestoreItem; // If we can still find the package for the old world, forcibly unload it too UPackage* const Package = FindPackage(nullptr, *CurrentWorldRestoreItem->GetPackageName()); if (Package) { PackagesToUnload.Add(Package); } } } UPackageTools::UnloadPackages(PackagesToUnload); } void ReloadPackagesAfterRestore(const FPackageRestoreItems& ContentPackagesToReload, const FPackageRestoreItemPtr& WorldPackageToReload) { // Reload any content packages that we unloaded to perform the restore for (auto It = ContentPackagesToReload.CreateConstIterator(); It; ++It) { const FPackageRestoreItemPtr& RestoreItem = *It; UPackageTools::LoadPackage(*RestoreItem->GetPackageName()); } // Also reload the current world if we caused it to be unloaded if (WorldPackageToReload.IsValid()) { FEditorFileUtils::LoadMap(WorldPackageToReload->GetPackageFilename()); } } } // namespace PackageRestore FEditorFileUtils::EPromptReturnCode PackageRestore::PromptToRestorePackages(const TMap& PackagesToRestore, TArray* OutFailedPackages) { const FString AutoSaveDir = AutoSaveUtils::GetAutoSaveDir(); FPackageRestoreItems PackageRestoreItems; PackageRestoreItems.Reserve(PackagesToRestore.Num()); for (auto It = PackagesToRestore.CreateConstIterator(); It; ++It) { const FString& PackageFullPath = It.Key(); const FString& AutoSavePath = It.Value(); FString PackageFilename; if (FPackageName::DoesPackageExist(PackageFullPath, nullptr, &PackageFilename)) { FPackageRestoreItemPtr PackageItemPtr = MakeShareable(new FPackageRestoreItem(PackageFullPath, PackageFilename, AutoSaveDir / AutoSavePath, true /*bIsExistingPackage*/)); PackageRestoreItems.Add(PackageItemPtr); } else { // A package may not exist on disk if it was for a newly added or imported asset, which hasn't yet had SaveDirtyPackages called for it if (FPackageName::TryConvertLongPackageNameToFilename(PackageFullPath, PackageFilename)) // no extension yet { PackageFilename += FPaths::GetExtension(AutoSavePath, true /*bIncludeDot*/); FPackageRestoreItemPtr PackageItemPtr = MakeShareable(new FPackageRestoreItem(PackageFullPath, PackageFilename, AutoSaveDir / AutoSavePath, false /*bIsExistingPackage*/)); PackageRestoreItems.Add(PackageItemPtr); } } } if (!PackageRestoreItems.Num()) { // Nothing to restore return FEditorFileUtils::PR_Success; } // Create the dlg to ask the user which packages to restore TSharedRef PackageRestoreDlgRef = SNew(SPackageRestoreDialog) .PackageRestoreItems(&PackageRestoreItems); // Create the window to host our dlg TSharedRef PackageRestoreWindowRef = SNew(SWindow) .Title(LOCTEXT("RestorePackages", "Restore Packages")) .ClientSize(FVector2D(900, 400)); PackageRestoreWindowRef->SetContent(PackageRestoreDlgRef); PackageRestoreDlgRef->SetWindow(PackageRestoreWindowRef); // Show the dlg in a modal window so we can wait for the result in this function GEditor->EditorAddModalWindow(PackageRestoreWindowRef); // Get the return code, and work out what we need to restore FPackageRestoreItems SelectedPackageItems; if (!PackageRestoreDlgRef->GetReturnType(SelectedPackageItems)) { return FEditorFileUtils::PR_Declined; } // Try and ensure that these packages are checked-out by the source control system { TArray SelectedPackageNames; SelectedPackageNames.Reserve(SelectedPackageItems.Num()); // Get an array of selected package names to check out for (auto It = SelectedPackageItems.CreateConstIterator(); It; ++It) { const FPackageRestoreItemPtr& SelectedPackageItem = *It; if (SelectedPackageItem->IsExistingPackage()) { SelectedPackageNames.Add(SelectedPackageItem->GetPackageName()); } } // Note: This may fail and present the user with an error message, however we still // want to continue as they may have checked out some packages that could now be restored const bool bErrorIfAlreadyCheckedOut = false; // some of the packages might already be checked out; that isn't an error FEditorFileUtils::CheckoutPackages(SelectedPackageNames, nullptr, bErrorIfAlreadyCheckedOut); } // It's possible that some packages may have already been loaded by the editor // If they have, we need to forcibly unload them so that we can overwrite their files FPackageRestoreItems ContentPackagesToReload; FPackageRestoreItemPtr WorldPackageToReload; UnloadPackagesBeforeRestore(SelectedPackageItems, ContentPackagesToReload, WorldPackageToReload); // Copy the auto-save files over the originals TArray FailedPackages; for (auto It = SelectedPackageItems.CreateConstIterator(); It; ++It) { const FPackageRestoreItemPtr& SelectedItem = *It; if (IFileManager::Get().Copy(*SelectedItem->GetPackageFilename(), *SelectedItem->GetAutoSaveFilename()) != COPY_OK) { FailedPackages.Add(SelectedItem->GetPackageName()); } } // Reload any packages that we unloaded above ReloadPackagesAfterRestore(ContentPackagesToReload, WorldPackageToReload); if (FailedPackages.Num()) { if (OutFailedPackages) { *OutFailedPackages = FailedPackages; } FString FailedPackagesStr; for (auto It = FailedPackages.CreateConstIterator(); It; ++It) { const FString& PackageName = *It; if (It.GetIndex() > 0) { FailedPackagesStr += "\n"; } FailedPackagesStr += PackageName; } FFormatNamedArguments Args; Args.Add(TEXT("FailedRestoreMessage"), LOCTEXT("FailedRestoreMessage", "The following packages could not be restored")); Args.Add(TEXT("FailedPackages"), FText::FromString(FailedPackagesStr)); const FText Message = FText::Format(FText::FromString("{FailedRestoreMessage}:\n{FailedPackages}"), Args); const FText Title = LOCTEXT("FailedRestoreDlgTitle", "Failed to restore packages!"); FMessageDialog::Open(EAppMsgType::Ok, Message, &Title); return FEditorFileUtils::PR_Failure; } return FEditorFileUtils::PR_Success; } #undef LOCTEXT_NAMESPACE