You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
This represents UE4/Main @17911760, Release-5.0 @17915875 and Dev-PerfTest @17914035 [CL 17918595 by aurel cordonnier in ue5-release-engine-test branch]
1639 lines
50 KiB
C++
1639 lines
50 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "PythonScriptPlugin.h"
|
|
#include "PythonScriptPluginSettings.h"
|
|
#include "PythonScriptPluginStyle.h"
|
|
#include "PythonScriptRemoteExecution.h"
|
|
#include "PyGIL.h"
|
|
#include "PyCore.h"
|
|
#include "PySlate.h"
|
|
#include "PyEngine.h"
|
|
#include "PyEditor.h"
|
|
#include "PyConstant.h"
|
|
#include "PyConversion.h"
|
|
#include "PyMethodWithClosure.h"
|
|
#include "PyReferenceCollector.h"
|
|
#include "PyWrapperTypeRegistry.h"
|
|
#include "EngineAnalytics.h"
|
|
#include "Interfaces/IAnalyticsProvider.h"
|
|
#include "AssetRegistryModule.h"
|
|
#include "Modules/ModuleManager.h"
|
|
#include "UObject/PackageReload.h"
|
|
#include "Misc/App.h"
|
|
#include "Misc/CoreDelegates.h"
|
|
#include "Misc/Char.h"
|
|
#include "Misc/CString.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/PackageName.h"
|
|
#include "HAL/PlatformMisc.h"
|
|
#include "HAL/PlatformProcess.h"
|
|
#include "Containers/Ticker.h"
|
|
#include "Features/IModularFeatures.h"
|
|
#include "ProfilingDebugging/ScopedTimers.h"
|
|
#include "Stats/Stats.h"
|
|
#include "String/Find.h"
|
|
|
|
#if WITH_EDITOR
|
|
#include "EditorSupportDelegates.h"
|
|
#include "DesktopPlatformModule.h"
|
|
#include "EditorStyleSet.h"
|
|
#include "Engine/Engine.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "Framework/MultiBox/MultiBoxBuilder.h"
|
|
#include "ToolMenus.h"
|
|
#include "Misc/ConfigCacheIni.h"
|
|
#include "Misc/MessageDialog.h"
|
|
#include "AssetViewUtils.h"
|
|
#include "IContentBrowserDataModule.h"
|
|
#include "ContentBrowserDataSubsystem.h"
|
|
#include "ContentBrowserFileDataCore.h"
|
|
#include "ContentBrowserFileDataSource.h"
|
|
#include "Toolkits/GlobalEditorCommonCommands.h"
|
|
#endif // WITH_EDITOR
|
|
|
|
#if PLATFORM_WINDOWS
|
|
#include <stdio.h>
|
|
#include <fcntl.h>
|
|
#include <io.h>
|
|
#endif // PLATFORM_WINDOWS
|
|
#include <locale.h>
|
|
|
|
#define LOCTEXT_NAMESPACE "PythonScriptPlugin"
|
|
|
|
#if WITH_PYTHON
|
|
|
|
static PyUtil::FPyApiBuffer NullPyArg = PyUtil::TCHARToPyApiBuffer(TEXT(""));
|
|
static PyUtil::FPyApiChar* NullPyArgPtrs[] = { NullPyArg.GetData() };
|
|
|
|
/** Util struct to set the sys.argv data for Python when executing a file with arguments */
|
|
struct FPythonScopedArgv
|
|
{
|
|
FPythonScopedArgv(const TCHAR* InArgs)
|
|
{
|
|
if (InArgs && *InArgs)
|
|
{
|
|
FString NextToken;
|
|
while (FParse::Token(InArgs, NextToken, false))
|
|
{
|
|
PyCommandLineArgs.Add(PyUtil::TCHARToPyApiBuffer(*NextToken));
|
|
}
|
|
|
|
PyCommandLineArgPtrs.Reserve(PyCommandLineArgs.Num());
|
|
for (PyUtil::FPyApiBuffer& PyCommandLineArg : PyCommandLineArgs)
|
|
{
|
|
PyCommandLineArgPtrs.Add(PyCommandLineArg.GetData());
|
|
}
|
|
}
|
|
PySys_SetArgvEx(PyCommandLineArgPtrs.Num(), PyCommandLineArgPtrs.GetData(), 0);
|
|
}
|
|
|
|
~FPythonScopedArgv()
|
|
{
|
|
PySys_SetArgvEx(1, NullPyArgPtrs, 0);
|
|
}
|
|
|
|
TArray<PyUtil::FPyApiBuffer> PyCommandLineArgs;
|
|
TArray<PyUtil::FPyApiChar*> PyCommandLineArgPtrs;
|
|
};
|
|
|
|
FPythonCommandExecutor::FPythonCommandExecutor(IPythonScriptPlugin* InPythonScriptPlugin)
|
|
: PythonScriptPlugin(InPythonScriptPlugin)
|
|
{
|
|
}
|
|
|
|
FName FPythonCommandExecutor::StaticName()
|
|
{
|
|
static const FName CmdExecName = TEXT("Python");
|
|
return CmdExecName;
|
|
}
|
|
|
|
FName FPythonCommandExecutor::GetName() const
|
|
{
|
|
return StaticName();
|
|
}
|
|
|
|
FText FPythonCommandExecutor::GetDisplayName() const
|
|
{
|
|
return LOCTEXT("PythonCommandExecutorDisplayName", "Python");
|
|
}
|
|
|
|
FText FPythonCommandExecutor::GetDescription() const
|
|
{
|
|
return LOCTEXT("PythonCommandExecutorDescription", "Execute Python scripts (including files)");
|
|
}
|
|
|
|
FText FPythonCommandExecutor::GetHintText() const
|
|
{
|
|
return LOCTEXT("PythonCommandExecutorHintText", "Enter Python script or a filename");
|
|
}
|
|
|
|
void FPythonCommandExecutor::GetAutoCompleteSuggestions(const TCHAR* Input, TArray<FString>& Out)
|
|
{
|
|
}
|
|
|
|
void FPythonCommandExecutor::GetExecHistory(TArray<FString>& Out)
|
|
{
|
|
IConsoleManager::Get().GetConsoleHistory(TEXT("Python"), Out);
|
|
}
|
|
|
|
bool FPythonCommandExecutor::Exec(const TCHAR* Input)
|
|
{
|
|
IConsoleManager::Get().AddConsoleHistoryEntry(TEXT("Python"), Input);
|
|
|
|
UE_LOG(LogPython, Log, TEXT("%s"), Input);
|
|
|
|
PythonScriptPlugin->ExecPythonCommand(Input);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FPythonCommandExecutor::AllowHotKeyClose() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool FPythonCommandExecutor::AllowMultiLine() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
FInputChord FPythonCommandExecutor::GetHotKey() const
|
|
{
|
|
#if WITH_EDITOR
|
|
return FGlobalEditorCommonCommands::Get().OpenConsoleCommandBox->GetActiveChord(EMultipleKeyBindingIndex::Primary).Get();
|
|
#else
|
|
return FInputChord();
|
|
#endif
|
|
}
|
|
|
|
FPythonREPLCommandExecutor::FPythonREPLCommandExecutor(IPythonScriptPlugin* InPythonScriptPlugin)
|
|
: PythonScriptPlugin(InPythonScriptPlugin)
|
|
{
|
|
}
|
|
|
|
FName FPythonREPLCommandExecutor::StaticName()
|
|
{
|
|
static const FName CmdExecName = TEXT("PythonREPL");
|
|
return CmdExecName;
|
|
}
|
|
|
|
FName FPythonREPLCommandExecutor::GetName() const
|
|
{
|
|
return StaticName();
|
|
}
|
|
|
|
FText FPythonREPLCommandExecutor::GetDisplayName() const
|
|
{
|
|
return LOCTEXT("PythonREPLCommandExecutorDisplayName", "Python (REPL)");
|
|
}
|
|
|
|
FText FPythonREPLCommandExecutor::GetDescription() const
|
|
{
|
|
return LOCTEXT("PythonREPLCommandExecutorDescription", "Execute a single Python statement and show its result");
|
|
}
|
|
|
|
FText FPythonREPLCommandExecutor::GetHintText() const
|
|
{
|
|
return LOCTEXT("PythonREPLCommandExecutorHintText", "Enter a Python statement");
|
|
}
|
|
|
|
void FPythonREPLCommandExecutor::GetAutoCompleteSuggestions(const TCHAR* Input, TArray<FString>& Out)
|
|
{
|
|
}
|
|
|
|
void FPythonREPLCommandExecutor::GetExecHistory(TArray<FString>& Out)
|
|
{
|
|
IConsoleManager::Get().GetConsoleHistory(TEXT("PythonREPL"), Out);
|
|
}
|
|
|
|
bool FPythonREPLCommandExecutor::Exec(const TCHAR* Input)
|
|
{
|
|
IConsoleManager::Get().AddConsoleHistoryEntry(TEXT("PythonREPL"), Input);
|
|
|
|
UE_LOG(LogPython, Log, TEXT("%s"), Input);
|
|
|
|
FPythonCommandEx PythonCommand;
|
|
PythonCommand.ExecutionMode = EPythonCommandExecutionMode::ExecuteStatement;
|
|
PythonCommand.Command = Input;
|
|
PythonScriptPlugin->ExecPythonCommandEx(PythonCommand);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FPythonREPLCommandExecutor::AllowHotKeyClose() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool FPythonREPLCommandExecutor::AllowMultiLine() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
FInputChord FPythonREPLCommandExecutor::GetHotKey() const
|
|
{
|
|
#if WITH_EDITOR
|
|
return FGlobalEditorCommonCommands::Get().OpenConsoleCommandBox->GetActiveChord(EMultipleKeyBindingIndex::Primary).Get();
|
|
#else
|
|
return FInputChord();
|
|
#endif
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
class FPythonCommandMenuImpl : public IPythonCommandMenu
|
|
{
|
|
public:
|
|
FPythonCommandMenuImpl()
|
|
: bRecentsFilesDirty(false)
|
|
{
|
|
}
|
|
|
|
virtual void OnStartupMenu() override
|
|
{
|
|
LoadConfig();
|
|
|
|
RegisterMenus();
|
|
}
|
|
|
|
virtual void OnShutdownMenu() override
|
|
{
|
|
UToolMenus::UnregisterOwner(this);
|
|
|
|
// Write to file
|
|
if (bRecentsFilesDirty)
|
|
{
|
|
SaveConfig();
|
|
bRecentsFilesDirty = false;
|
|
}
|
|
}
|
|
|
|
virtual void OnRunFile(const FString& InFile, bool bAdd) override
|
|
{
|
|
if (bAdd)
|
|
{
|
|
int32 Index = RecentsFiles.Find(InFile);
|
|
if (Index != INDEX_NONE)
|
|
{
|
|
// If already in the list but not at the last position
|
|
if (Index != RecentsFiles.Num() - 1)
|
|
{
|
|
RecentsFiles.RemoveAt(Index);
|
|
RecentsFiles.Add(InFile);
|
|
bRecentsFilesDirty = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (RecentsFiles.Num() >= MaxNumberOfFiles)
|
|
{
|
|
RecentsFiles.RemoveAt(0);
|
|
}
|
|
RecentsFiles.Add(InFile);
|
|
bRecentsFilesDirty = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (RecentsFiles.RemoveSingle(InFile) > 0)
|
|
{
|
|
bRecentsFilesDirty = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private:
|
|
const TCHAR* STR_ConfigSection = TEXT("Python");
|
|
const TCHAR* STR_ConfigDirectoryKey = TEXT("LastDirectory");
|
|
const FName NAME_ConfigRecentsFilesyKey = TEXT("RecentsFiles");
|
|
static const int32 MaxNumberOfFiles = 10;
|
|
|
|
void LoadConfig()
|
|
{
|
|
RecentsFiles.Reset();
|
|
|
|
GConfig->GetString(STR_ConfigSection, STR_ConfigDirectoryKey, LastDirectory, GEditorPerProjectIni);
|
|
|
|
FConfigSection* Sec = GConfig->GetSectionPrivate(STR_ConfigSection, false, true, GEditorPerProjectIni);
|
|
if (Sec)
|
|
{
|
|
TArray<FConfigValue> List;
|
|
Sec->MultiFind(NAME_ConfigRecentsFilesyKey, List);
|
|
|
|
int32 ListNum = FMath::Min(List.Num(), MaxNumberOfFiles);
|
|
|
|
RecentsFiles.Reserve(ListNum);
|
|
for (int32 Index = 0; Index < ListNum; ++Index)
|
|
{
|
|
RecentsFiles.Add(List[Index].GetValue());
|
|
}
|
|
}
|
|
}
|
|
|
|
void SaveConfig() const
|
|
{
|
|
GConfig->SetString(STR_ConfigSection, STR_ConfigDirectoryKey, *LastDirectory, GEditorPerProjectIni);
|
|
|
|
FConfigSection* Sec = GConfig->GetSectionPrivate(STR_ConfigSection, true, false, GEditorPerProjectIni);
|
|
if (Sec)
|
|
{
|
|
Sec->Remove(NAME_ConfigRecentsFilesyKey);
|
|
for (int32 Index = RecentsFiles.Num() - 1; Index >= 0; --Index)
|
|
{
|
|
Sec->Add(NAME_ConfigRecentsFilesyKey, *RecentsFiles[Index]);
|
|
}
|
|
}
|
|
|
|
GConfig->Flush(false);
|
|
}
|
|
|
|
void MakeRecentPythonScriptMenu(UToolMenu* InMenu)
|
|
{
|
|
FToolMenuOwnerScoped OwnerScoped(this);
|
|
FToolMenuSection& Section = InMenu->AddSection("Files");
|
|
for (int32 Index = RecentsFiles.Num() - 1; Index >= 0; --Index)
|
|
{
|
|
Section.AddMenuEntry(
|
|
NAME_None,
|
|
FText::FromString(RecentsFiles[Index]),
|
|
FText::GetEmpty(),
|
|
FSlateIcon(),
|
|
FUIAction(FExecuteAction::CreateRaw(this, &FPythonCommandMenuImpl::Menu_ExecutePythonRecent, Index))
|
|
);
|
|
}
|
|
|
|
FToolMenuSection& ClearSection = InMenu->AddSection("Clear");
|
|
Section.AddMenuEntry(
|
|
"ClearRecentPython",
|
|
LOCTEXT("ClearRecentPython", "Clear Recent Python Scripts"),
|
|
FText::GetEmpty(),
|
|
FSlateIcon(),
|
|
FUIAction(FExecuteAction::CreateRaw(this, &FPythonCommandMenuImpl::Menu_ClearRecentPython))
|
|
);
|
|
}
|
|
|
|
void RegisterMenus()
|
|
{
|
|
FToolMenuOwnerScoped OwnerScoped(this);
|
|
UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools");
|
|
FToolMenuSection& Section = Menu->AddSection("Python", LOCTEXT("Python", "Python"));//, FToolMenuInsert("FileLoadAndSave", EToolMenuInsertType::After));
|
|
Section.AddMenuEntry(
|
|
"OpenPython",
|
|
LOCTEXT("OpenPython", "Execute Python Script..."),
|
|
LOCTEXT("OpenPythonTooltip", "Open a Python Script file and Execute it."),
|
|
FSlateIcon(FPythonScriptPluginEditorStyle::Get().GetStyleSetName(), "Icons.PythonExecute"),
|
|
FUIAction(FExecuteAction::CreateRaw(this, &FPythonCommandMenuImpl::Menu_ExecutePython))
|
|
);
|
|
Section.AddSubMenu(
|
|
"RecentPythonsSubMenu",
|
|
LOCTEXT("RecentPythonsSubMenu", "Recent Python Scripts"),
|
|
LOCTEXT("RecentPythonsSubMenu_ToolTip", "Select a recent Python Script file and Execute it."),
|
|
FNewToolMenuDelegate::CreateRaw(this, &FPythonCommandMenuImpl::MakeRecentPythonScriptMenu), false,
|
|
FSlateIcon(FPythonScriptPluginEditorStyle::Get().GetStyleSetName(), "Icons.PythonRecent")
|
|
);
|
|
}
|
|
|
|
void Menu_ExecutePythonRecent(int32 Index)
|
|
{
|
|
if (RecentsFiles.IsValidIndex(Index))
|
|
{
|
|
FString PyCopied = RecentsFiles[Index];
|
|
GEngine->Exec(NULL, *FString::Printf(TEXT("py \"%s\""), *PyCopied));
|
|
}
|
|
}
|
|
|
|
void Menu_ClearRecentPython()
|
|
{
|
|
if (RecentsFiles.Num() > 0)
|
|
{
|
|
RecentsFiles.Reset();
|
|
bRecentsFilesDirty = true;
|
|
}
|
|
}
|
|
|
|
void Menu_ExecutePython()
|
|
{
|
|
TArray<FString> OpenedFiles;
|
|
FString DefaultDirectory = LastDirectory;
|
|
|
|
IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get();
|
|
if (DesktopPlatform)
|
|
{
|
|
bool bOpened = DesktopPlatform->OpenFileDialog(
|
|
FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr),
|
|
LOCTEXT("ExecutePython", "Execute Python Script").ToString(),
|
|
DefaultDirectory,
|
|
TEXT(""),
|
|
TEXT("Python files|*.py|"),
|
|
EFileDialogFlags::None,
|
|
OpenedFiles
|
|
);
|
|
|
|
if (bOpened && OpenedFiles.Num() > 0)
|
|
{
|
|
if (DefaultDirectory != LastDirectory)
|
|
{
|
|
LastDirectory = DefaultDirectory;
|
|
bRecentsFilesDirty = true;
|
|
}
|
|
|
|
GEngine->Exec(NULL, *FString::Printf(TEXT("py \"%s\""), *OpenedFiles.Last()));
|
|
}
|
|
}
|
|
}
|
|
|
|
private:
|
|
|
|
TArray<FString> RecentsFiles;
|
|
FString LastDirectory;
|
|
|
|
bool bRecentsFilesDirty;
|
|
};
|
|
#endif // WITH_EDITOR
|
|
|
|
#endif // WITH_PYTHON
|
|
|
|
FPythonScriptPlugin::FPythonScriptPlugin()
|
|
#if WITH_PYTHON
|
|
: CmdExec(this)
|
|
, CmdREPLExec(this)
|
|
, CmdMenu(nullptr)
|
|
, bInitialized(false)
|
|
, bHasTicked(false)
|
|
#endif // WITH_PYTHON
|
|
{
|
|
}
|
|
|
|
bool FPythonScriptPlugin::IsPythonAvailable() const
|
|
{
|
|
static const bool bDisablePython = FParse::Param(FCommandLine::Get(), TEXT("DisablePython"));
|
|
return WITH_PYTHON && !bDisablePython;
|
|
}
|
|
|
|
bool FPythonScriptPlugin::ExecPythonCommand(const TCHAR* InPythonCommand)
|
|
{
|
|
FPythonCommandEx PythonCommand;
|
|
PythonCommand.Command = InPythonCommand;
|
|
return ExecPythonCommandEx(PythonCommand);
|
|
}
|
|
|
|
bool FPythonScriptPlugin::ExecPythonCommandEx(FPythonCommandEx& InOutPythonCommand)
|
|
{
|
|
#if WITH_PYTHON
|
|
if (!IsPythonAvailable())
|
|
#endif
|
|
{
|
|
InOutPythonCommand.CommandResult = TEXT("Python is not available!");
|
|
ensureAlwaysMsgf(false, TEXT("%s"), *InOutPythonCommand.CommandResult);
|
|
return false;
|
|
}
|
|
|
|
#if WITH_PYTHON
|
|
if (InOutPythonCommand.ExecutionMode == EPythonCommandExecutionMode::ExecuteFile)
|
|
{
|
|
// The EPythonCommandExecutionMode::ExecuteFile name is misleading as it is used to run literal code or a .py file. Detect
|
|
// if the user supplied a .py files. The command can have the python pathname with spaces, command line parameter(s) and could be quoted.
|
|
// C:\My Scripts\Test.py -param1 -param2 -> Ok
|
|
// "C:\My Scripts\Test.py " -param1 -param2 -> Ok
|
|
// "C:\My Scripts\Test.py"-param1 -param2 -> Ok
|
|
// C:\My Scripts\Test.py-param1 -param2 -> Error missing a space between .py and -param1
|
|
// "C:\My Scripts\Test.py -> Error missing closing quote.
|
|
// C:\My Scripts\Test.py " -> Error missing opening quote.
|
|
auto TryExtractPathnameAndCommand = [&InOutPythonCommand](FString& OutExtractedFilename, FString& OutExtractedCommand) -> bool
|
|
{
|
|
const TCHAR* PyFileExtension = TEXT(".py");
|
|
int32 Pos = UE::String::FindFirst(InOutPythonCommand.Command, PyFileExtension);
|
|
if (Pos == INDEX_NONE)
|
|
{
|
|
return false; // no .py file extension found.
|
|
}
|
|
|
|
int32 EndPathnamePos = Pos + FCString::Strlen(PyFileExtension);
|
|
OutExtractedFilename = InOutPythonCommand.Command.Left(EndPathnamePos);
|
|
bool bCommandQuoted = false;
|
|
|
|
// The caller may quote the pathname if it contains space(s). Trim a leading quote, if any. (Any trailing quote is already removed by the Left() command).
|
|
if (OutExtractedFilename.StartsWith(TEXT("\"")))
|
|
{
|
|
OutExtractedFilename.RemoveAt(0);
|
|
bCommandQuoted = true;
|
|
}
|
|
|
|
// Early out if the command doesn't match an existing file.
|
|
if (!IFileManager::Get().FileExists(*OutExtractedFilename))
|
|
{
|
|
return false;
|
|
}
|
|
// If the pathname started with a quote, expect a closing quote after the .py.
|
|
else if (bCommandQuoted)
|
|
{
|
|
if (EndPathnamePos == InOutPythonCommand.Command.Len())
|
|
{
|
|
return false; // Missing the closing quote.
|
|
}
|
|
|
|
// Scan after the .py to find the closing quote.
|
|
for (int32 CurrPos = EndPathnamePos; CurrPos < InOutPythonCommand.Command.Len(); ++CurrPos)
|
|
{
|
|
if (InOutPythonCommand.Command[CurrPos] == TEXT('\"'))
|
|
{
|
|
EndPathnamePos = CurrPos + 1; // +1 to put the end just after the quote like a std::end() iterator.
|
|
break;
|
|
}
|
|
else if (FChar::IsWhitespace(InOutPythonCommand.Command[CurrPos]))
|
|
{
|
|
continue; // It is legal to have blank space after the .py.
|
|
}
|
|
else
|
|
{
|
|
return false; // Invalid character found after .py.
|
|
}
|
|
}
|
|
}
|
|
// Some characters appears after the .py
|
|
else if (EndPathnamePos < InOutPythonCommand.Command.Len() && !FChar::IsWhitespace(InOutPythonCommand.Command[EndPathnamePos]))
|
|
{
|
|
return false; // Some non-blank characters are there and we don't expect a closing quote. This is not a valid command Ex: C:\MyScript.py-t
|
|
}
|
|
|
|
// Quote/re-quote the command. This allows Python to set sys.argv/sys.argc property if the pathname contains space. (So parts of the pathname are not interpreted as arguments)
|
|
OutExtractedCommand = FString::Printf(TEXT("\"%s\""), *OutExtractedFilename);
|
|
|
|
// Append the arguments (if any).
|
|
if (EndPathnamePos < InOutPythonCommand.Command.Len())
|
|
{
|
|
OutExtractedCommand += InOutPythonCommand.Command.Mid(EndPathnamePos);
|
|
}
|
|
|
|
return true; // Pathname, command and argument were successfully parsed.
|
|
};
|
|
|
|
FString ExtractedFilename;
|
|
FString ExtractedCommand;
|
|
|
|
if (TryExtractPathnameAndCommand(ExtractedFilename, ExtractedCommand))
|
|
{
|
|
return RunFile(*ExtractedFilename, *ExtractedCommand, InOutPythonCommand);
|
|
}
|
|
else
|
|
{
|
|
return RunString(InOutPythonCommand);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return RunString(InOutPythonCommand);
|
|
}
|
|
#endif // WITH_PYTHON
|
|
}
|
|
|
|
FSimpleMulticastDelegate& FPythonScriptPlugin::OnPythonInitialized()
|
|
{
|
|
return OnPythonInitializedDelegate;
|
|
}
|
|
|
|
FSimpleMulticastDelegate& FPythonScriptPlugin::OnPythonShutdown()
|
|
{
|
|
return OnPythonShutdownDelegate;
|
|
}
|
|
|
|
void FPythonScriptPlugin::StartupModule()
|
|
{
|
|
if (!IsPythonAvailable())
|
|
{
|
|
return;
|
|
}
|
|
|
|
#if WITH_PYTHON
|
|
InitializePython();
|
|
IModularFeatures::Get().RegisterModularFeature(IConsoleCommandExecutor::ModularFeatureName(), &CmdExec);
|
|
IModularFeatures::Get().RegisterModularFeature(IConsoleCommandExecutor::ModularFeatureName(), &CmdREPLExec);
|
|
|
|
check(!RemoteExecution);
|
|
RemoteExecution = MakeUnique<FPythonScriptRemoteExecution>(this);
|
|
|
|
#if WITH_EDITOR
|
|
FPythonScriptPluginEditorStyle::Get();
|
|
FCoreDelegates::OnPostEngineInit.AddRaw(this, &FPythonScriptPlugin::OnPostEngineInit);
|
|
#endif // WITH_EDITOR
|
|
|
|
FCoreDelegates::OnPreExit.AddRaw(this, &FPythonScriptPlugin::ShutdownPython);
|
|
#endif // WITH_PYTHON
|
|
}
|
|
|
|
#if WITH_PYTHON
|
|
#if WITH_EDITOR
|
|
void FPythonScriptPlugin::OnPostEngineInit()
|
|
{
|
|
if (UToolMenus::IsToolMenuUIEnabled())
|
|
{
|
|
check(CmdMenu == nullptr);
|
|
CmdMenu = new FPythonCommandMenuImpl();
|
|
CmdMenu->OnStartupMenu();
|
|
|
|
UToolMenus::Get()->RegisterStringCommandHandler("Python", FToolMenuExecuteString::CreateLambda([this](const FString& InString, const FToolMenuContext& InContext) {
|
|
ExecPythonCommand(*InString);
|
|
}));
|
|
}
|
|
}
|
|
|
|
#endif // WITH_EDITOR
|
|
#endif // WITH_PYTHON
|
|
|
|
void FPythonScriptPlugin::ShutdownModule()
|
|
{
|
|
if (!IsPythonAvailable())
|
|
{
|
|
return;
|
|
}
|
|
|
|
#if WITH_PYTHON
|
|
FCoreDelegates::OnPreExit.RemoveAll(this);
|
|
|
|
RemoteExecution.Reset();
|
|
|
|
#if WITH_EDITOR
|
|
FCoreDelegates::OnPostEngineInit.RemoveAll(this);
|
|
|
|
if (CmdMenu)
|
|
{
|
|
CmdMenu->OnShutdownMenu();
|
|
delete CmdMenu;
|
|
CmdMenu = nullptr;
|
|
}
|
|
|
|
if (UToolMenus* ToolMenus = UToolMenus::TryGet())
|
|
{
|
|
ToolMenus->UnregisterStringCommandHandler("Python");
|
|
}
|
|
#endif // WITH_EDITOR
|
|
|
|
IModularFeatures::Get().UnregisterModularFeature(IConsoleCommandExecutor::ModularFeatureName(), &CmdExec);
|
|
IModularFeatures::Get().UnregisterModularFeature(IConsoleCommandExecutor::ModularFeatureName(), &CmdREPLExec);
|
|
ShutdownPython();
|
|
#endif // WITH_PYTHON
|
|
}
|
|
|
|
bool FPythonScriptPlugin::Exec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar)
|
|
{
|
|
#if WITH_PYTHON
|
|
if (FParse::Command(&Cmd, TEXT("PY")))
|
|
{
|
|
ExecPythonCommand(Cmd);
|
|
return true;
|
|
}
|
|
#endif // WITH_PYTHON
|
|
return false;
|
|
}
|
|
|
|
void FPythonScriptPlugin::PreChange(const UUserDefinedEnum* Enum, FEnumEditorUtils::EEnumEditorChangeInfo Info)
|
|
{
|
|
}
|
|
|
|
void FPythonScriptPlugin::PostChange(const UUserDefinedEnum* Enum, FEnumEditorUtils::EEnumEditorChangeInfo Info)
|
|
{
|
|
#if WITH_PYTHON
|
|
OnAssetUpdated(Enum);
|
|
#endif //WITH_PYTHON
|
|
}
|
|
|
|
#if WITH_PYTHON
|
|
|
|
void FPythonScriptPlugin::InitializePython()
|
|
{
|
|
bInitialized = true;
|
|
|
|
// Set-up the correct program name
|
|
{
|
|
FString ProgramName = FPlatformProcess::GetCurrentWorkingDirectory() / FPlatformProcess::ExecutableName(false);
|
|
FPaths::NormalizeFilename(ProgramName);
|
|
PyProgramName = PyUtil::TCHARToPyApiBuffer(*ProgramName);
|
|
}
|
|
|
|
// Set-up the correct home path
|
|
{
|
|
// Build the full Python directory (UE_PYTHON_DIR may be relative to the engine directory for portability)
|
|
FString PythonDir = UTF8_TO_TCHAR(UE_PYTHON_DIR);
|
|
PythonDir.ReplaceInline(TEXT("{ENGINE_DIR}"), *FPaths::EngineDir(), ESearchCase::CaseSensitive);
|
|
FPaths::NormalizeDirectoryName(PythonDir);
|
|
FPaths::RemoveDuplicateSlashes(PythonDir);
|
|
PyHomePath = PyUtil::TCHARToPyApiBuffer(*PythonDir);
|
|
}
|
|
|
|
// Initialize the Python interpreter
|
|
{
|
|
UE_LOG(LogPython, Log, TEXT("Using Python %d.%d.%d"), PY_MAJOR_VERSION, PY_MINOR_VERSION, PY_MICRO_VERSION);
|
|
|
|
// Python 3 changes the console mode from O_TEXT to O_BINARY which affects other uses of the console
|
|
// So change the console mode back to its current setting after Py_Initialize has been called
|
|
#if PLATFORM_WINDOWS && PY_MAJOR_VERSION >= 3
|
|
// We call _setmode here to cache the current state
|
|
CA_SUPPRESS(6031)
|
|
fflush(stdin);
|
|
const int StdInMode = _setmode(_fileno(stdin), _O_TEXT);
|
|
CA_SUPPRESS(6031)
|
|
fflush(stdout);
|
|
const int StdOutMode = _setmode(_fileno(stdout), _O_TEXT);
|
|
CA_SUPPRESS(6031)
|
|
fflush(stderr);
|
|
const int StdErrMode = _setmode(_fileno(stderr), _O_TEXT);
|
|
#endif // PLATFORM_WINDOWS && PY_MAJOR_VERSION >= 3
|
|
|
|
#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 7
|
|
// Python 3.7+ changes the C locale which affects functions using C string APIs
|
|
// So change the C locale back to its current setting after Py_Initialize has been called
|
|
FString CurrentLocale;
|
|
if (const char* CurrentLocalePtr = setlocale(LC_ALL, nullptr))
|
|
{
|
|
CurrentLocale = ANSI_TO_TCHAR(CurrentLocalePtr);
|
|
}
|
|
#endif // PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 7
|
|
|
|
Py_IgnoreEnvironmentFlag = 1; // 1 so Python doesn't inherit its environment variables into our embedded interpreter
|
|
#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 4
|
|
Py_IsolatedFlag = 1; // 1 so Python doesn't add the user scripts and site-packages paths into our embedded interpreter
|
|
Py_SetStandardStreamEncoding("utf-8", nullptr);
|
|
#endif // PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 4
|
|
Py_SetProgramName(PyProgramName.GetData());
|
|
Py_SetPythonHome(PyHomePath.GetData());
|
|
Py_InitializeEx(0); // 0 so Python doesn't override any signal handling
|
|
|
|
#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION < 7
|
|
// NOTE: Since 3.7, those functions are called by Py_InitializeEx()
|
|
//
|
|
// Ensure Python supports multiple threads via the GIL, as UE GC runs over multiple threads,
|
|
// which may invoke FPyReferenceCollector::AddReferencedObjects on a background thread...
|
|
if (!PyEval_ThreadsInitialized())
|
|
{
|
|
PyEval_InitThreads();
|
|
}
|
|
#endif // PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION < 7
|
|
|
|
#if PLATFORM_WINDOWS && PY_MAJOR_VERSION >= 3
|
|
// We call _setmode here to restore the previous state
|
|
if (StdInMode != -1)
|
|
{
|
|
CA_SUPPRESS(6031)
|
|
fflush(stdin);
|
|
CA_SUPPRESS(6031)
|
|
_setmode(_fileno(stdin), StdInMode);
|
|
}
|
|
if (StdOutMode != -1)
|
|
{
|
|
CA_SUPPRESS(6031)
|
|
fflush(stdout);
|
|
CA_SUPPRESS(6031)
|
|
_setmode(_fileno(stdout), StdOutMode);
|
|
}
|
|
if (StdErrMode != -1)
|
|
{
|
|
CA_SUPPRESS(6031)
|
|
fflush(stderr);
|
|
CA_SUPPRESS(6031)
|
|
_setmode(_fileno(stderr), StdErrMode);
|
|
}
|
|
#endif // PLATFORM_WINDOWS && PY_MAJOR_VERSION >= 3
|
|
|
|
#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 7
|
|
// We call setlocale here to restore the previous state
|
|
if (!CurrentLocale.IsEmpty())
|
|
{
|
|
setlocale(LC_ALL, TCHAR_TO_ANSI(*CurrentLocale));
|
|
}
|
|
#endif // PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 7
|
|
|
|
PySys_SetArgvEx(1, NullPyArgPtrs, 0);
|
|
|
|
// Enable developer warnings if requested
|
|
if (IsDeveloperModeEnabled())
|
|
{
|
|
PyUtil::EnableDeveloperWarnings();
|
|
}
|
|
|
|
// Initialize our custom method type as we'll need it when generating bindings
|
|
InitializePyMethodWithClosure();
|
|
|
|
// Initialize our custom constant type as we'll need it when generating bindings
|
|
InitializePyConstant();
|
|
|
|
PyObject* PyMainModule = PyImport_AddModule("__main__");
|
|
PyDefaultGlobalDict = FPyObjectPtr::NewReference(PyModule_GetDict(PyMainModule));
|
|
PyDefaultLocalDict = PyDefaultGlobalDict;
|
|
|
|
PyConsoleGlobalDict = FPyObjectPtr::StealReference(PyDict_Copy(PyDefaultGlobalDict));
|
|
PyConsoleLocalDict = PyConsoleGlobalDict;
|
|
|
|
#if WITH_EDITOR
|
|
FEditorSupportDelegates::PrepareToCleanseEditorObject.AddRaw(this, &FPythonScriptPlugin::OnPrepareToCleanseEditorObject);
|
|
#endif // WITH_EDITOR
|
|
}
|
|
|
|
// Set-up the known Python script paths
|
|
{
|
|
PyUtil::AddSystemPath(FPaths::ConvertRelativePathToFull(FPlatformProcess::UserDir() / FApp::GetEpicProductIdentifier() / TEXT("Python")));
|
|
|
|
TArray<FString> RootPaths;
|
|
FPackageName::QueryRootContentPaths(RootPaths);
|
|
for (const FString& RootPath : RootPaths)
|
|
{
|
|
const FString RootFilesystemPath = FPackageName::LongPackageNameToFilename(RootPath);
|
|
RegisterModulePaths(RootFilesystemPath);
|
|
}
|
|
|
|
for (const FDirectoryPath& AdditionalPath : GetDefault<UPythonScriptPluginSettings>()->AdditionalPaths)
|
|
{
|
|
PyUtil::AddSystemPath(FPaths::ConvertRelativePathToFull(AdditionalPath.Path));
|
|
}
|
|
|
|
TArray<FString> SystemEnvPaths;
|
|
FPlatformMisc::GetEnvironmentVariable(TEXT("UE_PYTHONPATH")).ParseIntoArray(SystemEnvPaths, FPlatformMisc::GetPathVarDelimiter());
|
|
for (const FString& SystemEnvPath : SystemEnvPaths)
|
|
{
|
|
PyUtil::AddSystemPath(SystemEnvPath);
|
|
}
|
|
|
|
FPackageName::OnContentPathMounted().AddRaw(this, &FPythonScriptPlugin::OnContentPathMounted);
|
|
FPackageName::OnContentPathDismounted().AddRaw(this, &FPythonScriptPlugin::OnContentPathDismounted);
|
|
FCoreUObjectDelegates::OnPackageReloaded.AddRaw(this, &FPythonScriptPlugin::OnAssetReload);
|
|
|
|
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
|
|
AssetRegistryModule.Get().OnAssetRenamed().AddRaw(this, &FPythonScriptPlugin::OnAssetRenamed);
|
|
AssetRegistryModule.Get().OnAssetRemoved().AddRaw(this, &FPythonScriptPlugin::OnAssetRemoved);
|
|
}
|
|
|
|
// Initialize the Unreal Python module
|
|
{
|
|
// Create the top-level "unreal" module
|
|
PyUnrealModule = FPyObjectPtr::NewReference(PyImport_AddModule("unreal"));
|
|
|
|
// Import "unreal" into the console by default
|
|
PyDict_SetItemString(PyConsoleGlobalDict, "unreal", PyUnrealModule);
|
|
|
|
// Initialize the and import the "core" module
|
|
PyCore::InitializeModule();
|
|
ImportUnrealModule(TEXT("core"));
|
|
|
|
// Initialize the and import the "slate" module
|
|
PySlate::InitializeModule();
|
|
ImportUnrealModule(TEXT("slate"));
|
|
|
|
// Initialize the and import the "engine" module
|
|
PyEngine::InitializeModule();
|
|
ImportUnrealModule(TEXT("engine"));
|
|
|
|
#if WITH_EDITOR
|
|
// Initialize the and import the "editor" module
|
|
PyEditor::InitializeModule();
|
|
ImportUnrealModule(TEXT("editor"));
|
|
#endif // WITH_EDITOR
|
|
|
|
FPyWrapperTypeRegistry::Get().OnModuleDirtied().AddRaw(this, &FPythonScriptPlugin::OnModuleDirtied);
|
|
FModuleManager::Get().OnModulesChanged().AddRaw(this, &FPythonScriptPlugin::OnModulesChanged);
|
|
|
|
// Initialize the wrapped types
|
|
FPyWrapperTypeRegistry::Get().GenerateWrappedTypes();
|
|
|
|
// Initialize the tick handler
|
|
TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda([this](float DeltaTime)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_FPythonScriptPlugin_Tick);
|
|
Tick(DeltaTime);
|
|
return true;
|
|
}));
|
|
}
|
|
|
|
// Release the GIL taken by Py_Initialize now that initialization has finished, to allow other threads access to Python
|
|
// We have to take this again prior to calling Py_Finalize, and all other code will lock on-demand via FPyScopedGIL
|
|
PyMainThreadState = PyEval_SaveThread();
|
|
|
|
#if WITH_EDITOR
|
|
// Initialize the Content Browser integration
|
|
if (GIsEditor && !IsRunningCommandlet() && GetDefault<UPythonScriptPluginUserSettings>()->bEnableContentBrowserIntegration)
|
|
{
|
|
ContentBrowserFileData::FFileConfigData PythonFileConfig;
|
|
{
|
|
auto PyItemPassesFilter = [](const FName InFilePath, const FString& InFilename, const FContentBrowserDataFilter& InFilter, const bool bIsFile)
|
|
{
|
|
const FContentBrowserDataPackageFilter* PackageFilter = InFilter.ExtraFilters.FindFilter<FContentBrowserDataPackageFilter>();
|
|
if (PackageFilter && PackageFilter->PathPermissionList && PackageFilter->PathPermissionList->HasFiltering())
|
|
{
|
|
return PackageFilter->PathPermissionList->PassesStartsWithFilter(InFilePath, /*bAllowParentPaths*/!bIsFile);
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
auto GetPyItemAttribute = [](const FName InFilePath, const FString& InFilename, const bool InIncludeMetaData, const FName InAttributeKey, FContentBrowserItemDataAttributeValue& OutAttributeValue)
|
|
{
|
|
// TODO: Need to way to avoid all this ToString() churn (FPackageNameView?)
|
|
|
|
if (InAttributeKey == ContentBrowserItemAttributes::ItemIsDeveloperContent)
|
|
{
|
|
const bool bIsDevelopersFolder = AssetViewUtils::IsDevelopersFolder(InFilePath.ToString());
|
|
OutAttributeValue.SetValue(bIsDevelopersFolder);
|
|
return true;
|
|
}
|
|
|
|
if (InAttributeKey == ContentBrowserItemAttributes::ItemIsLocalizedContent)
|
|
{
|
|
const bool bIsLocalizedFolder = FPackageName::IsLocalizedPackage(InFilePath.ToString());
|
|
OutAttributeValue.SetValue(bIsLocalizedFolder);
|
|
return true;
|
|
}
|
|
|
|
if (InAttributeKey == ContentBrowserItemAttributes::ItemIsEngineContent)
|
|
{
|
|
const bool bIsEngineFolder = AssetViewUtils::IsEngineFolder(InFilePath.ToString(), /*bIncludePlugins*/true);
|
|
OutAttributeValue.SetValue(bIsEngineFolder);
|
|
return true;
|
|
}
|
|
|
|
if (InAttributeKey == ContentBrowserItemAttributes::ItemIsProjectContent)
|
|
{
|
|
const bool bIsProjectFolder = AssetViewUtils::IsProjectFolder(InFilePath.ToString(), /*bIncludePlugins*/true);
|
|
OutAttributeValue.SetValue(bIsProjectFolder);
|
|
return true;
|
|
}
|
|
|
|
if (InAttributeKey == ContentBrowserItemAttributes::ItemIsPluginContent)
|
|
{
|
|
const bool bIsPluginFolder = AssetViewUtils::IsPluginFolder(InFilePath.ToString());
|
|
OutAttributeValue.SetValue(bIsPluginFolder);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
auto PyItemPreview = [this](const FName InFilePath, const FString& InFilename)
|
|
{
|
|
ExecPythonCommand(*InFilename);
|
|
return true;
|
|
};
|
|
|
|
ContentBrowserFileData::FDirectoryActions PyDirectoryActions;
|
|
PyDirectoryActions.PassesFilter.BindLambda(PyItemPassesFilter, false);
|
|
PyDirectoryActions.GetAttribute.BindLambda(GetPyItemAttribute);
|
|
PythonFileConfig.SetDirectoryActions(PyDirectoryActions);
|
|
|
|
ContentBrowserFileData::FFileActions PyFileActions;
|
|
PyFileActions.TypeExtension = TEXT("py");
|
|
PyFileActions.TypeName = "Python";
|
|
PyFileActions.TypeDisplayName = LOCTEXT("PythonTypeName", "Python");
|
|
PyFileActions.TypeShortDescription = LOCTEXT("PythonTypeShortDescription", "Python Script");
|
|
PyFileActions.TypeFullDescription = LOCTEXT("PythonTypeFullDescription", "A file used to script the editor using Python");
|
|
PyFileActions.DefaultNewFileName = TEXT("new_python_script");
|
|
PyFileActions.TypeColor = FColor(255, 156, 0);
|
|
PyFileActions.PassesFilter.BindLambda(PyItemPassesFilter, true);
|
|
PyFileActions.GetAttribute.BindLambda(GetPyItemAttribute);
|
|
PyFileActions.Preview.BindLambda(PyItemPreview);
|
|
PythonFileConfig.RegisterFileActions(PyFileActions);
|
|
}
|
|
|
|
PythonFileDataSource.Reset(NewObject<UContentBrowserFileDataSource>(GetTransientPackage(), "PythonData"));
|
|
PythonFileDataSource->Initialize(PythonFileConfig);
|
|
|
|
TArray<FString> RootPaths;
|
|
FPackageName::QueryRootContentPaths(RootPaths);
|
|
for (const FString& RootPath : RootPaths)
|
|
{
|
|
const FString RootFilesystemPath = FPackageName::LongPackageNameToFilename(RootPath);
|
|
PythonFileDataSource->AddFileMount(*(RootPath / TEXT("Python")), RootFilesystemPath / TEXT("Python"));
|
|
}
|
|
|
|
{
|
|
FToolMenuOwnerScoped OwnerScoped(this);
|
|
if (UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("ContentBrowser.ItemContextMenu.PythonData"))
|
|
{
|
|
Menu->AddDynamicSection(TEXT("DynamicSection_PythonScriptPlugin"), FNewToolMenuDelegate::CreateRaw(this, &FPythonScriptPlugin::PopulatePythonFileContextMenu));
|
|
}
|
|
}
|
|
}
|
|
#endif // WITH_EDITOR
|
|
}
|
|
|
|
void FPythonScriptPlugin::ShutdownPython()
|
|
{
|
|
if (!bInitialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We need to restore the original GIL prior to calling Py_Finalize
|
|
PyEval_RestoreThread(PyMainThreadState);
|
|
PyMainThreadState = nullptr;
|
|
|
|
#if WITH_EDITOR
|
|
// Remove the Content Browser integration
|
|
UToolMenus::UnregisterOwner(this);
|
|
PythonFileDataSource.Reset();
|
|
#endif // WITH_EDITOR
|
|
|
|
// Notify any external listeners
|
|
OnPythonShutdownDelegate.Broadcast();
|
|
|
|
FTSTicker::GetCoreTicker().RemoveTicker(TickHandle);
|
|
if (ModuleDelayedHandle.IsValid())
|
|
{
|
|
FTSTicker::GetCoreTicker().RemoveTicker(ModuleDelayedHandle);
|
|
}
|
|
|
|
FPyWrapperTypeRegistry::Get().OnModuleDirtied().RemoveAll(this);
|
|
FModuleManager::Get().OnModulesChanged().RemoveAll(this);
|
|
|
|
FPackageName::OnContentPathMounted().RemoveAll(this);
|
|
FPackageName::OnContentPathDismounted().RemoveAll(this);
|
|
FCoreUObjectDelegates::OnPackageReloaded.RemoveAll(this);
|
|
|
|
if (FAssetRegistryModule* AssetRegistryModule = FModuleManager::GetModulePtr<FAssetRegistryModule>("AssetRegistry"))
|
|
{
|
|
AssetRegistryModule->Get().OnAssetRenamed().RemoveAll(this);
|
|
AssetRegistryModule->Get().OnAssetRemoved().RemoveAll(this);
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
FEditorSupportDelegates::PrepareToCleanseEditorObject.RemoveAll(this);
|
|
#endif // WITH_EDITOR
|
|
|
|
FPyReferenceCollector::Get().PurgeUnrealGeneratedTypes();
|
|
|
|
#if WITH_EDITOR
|
|
PyEditor::ShutdownModule();
|
|
#endif // WITH_EDITOR
|
|
PyEngine::ShutdownModule();
|
|
PySlate::ShutdownModule();
|
|
PyCore::ShutdownModule();
|
|
|
|
PyUnrealModule.Reset();
|
|
PyDefaultGlobalDict.Reset();
|
|
PyDefaultLocalDict.Reset();
|
|
PyConsoleGlobalDict.Reset();
|
|
PyConsoleLocalDict.Reset();
|
|
|
|
ShutdownPyMethodWithClosure();
|
|
|
|
Py_Finalize();
|
|
|
|
bInitialized = false;
|
|
bHasTicked = false;
|
|
}
|
|
|
|
void FPythonScriptPlugin::RequestStubCodeGeneration()
|
|
{
|
|
// Ignore requests made before the fist Tick
|
|
if (!bHasTicked)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Delay 2 seconds before generating as this may be triggered by loading several modules at once
|
|
static const float Delay = 2.0f;
|
|
|
|
// If there is an existing pending notification, remove it so that it can be reset
|
|
if (ModuleDelayedHandle.IsValid())
|
|
{
|
|
FTSTicker::GetCoreTicker().RemoveTicker(ModuleDelayedHandle);
|
|
ModuleDelayedHandle.Reset();
|
|
}
|
|
|
|
// Set new tick
|
|
ModuleDelayedHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda(
|
|
[this](float DeltaTime)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_FPythonScriptPlugin_ModuleDelayed);
|
|
|
|
// Once ticked, the delegate will be removed so reset the handle to indicate that it isn't set.
|
|
ModuleDelayedHandle.Reset();
|
|
|
|
// Call the event now that the delay has passed.
|
|
GenerateStubCode();
|
|
|
|
// Don't reschedule to run again.
|
|
return false;
|
|
}),
|
|
Delay);
|
|
}
|
|
|
|
void FPythonScriptPlugin::GenerateStubCode()
|
|
{
|
|
if (IsDeveloperModeEnabled())
|
|
{
|
|
// Generate stub code if developer mode enabled
|
|
FPyWrapperTypeRegistry::Get().GenerateStubCodeForWrappedTypes();
|
|
}
|
|
}
|
|
|
|
void FPythonScriptPlugin::Tick(const float InDeltaTime)
|
|
{
|
|
// If this is our first Tick, handle any post-init logic that should happen once the engine is fully initialized
|
|
if (!bHasTicked)
|
|
{
|
|
bHasTicked = true;
|
|
|
|
// Run start-up scripts now
|
|
TArray<FString> PySysPaths;
|
|
{
|
|
FPyScopedGIL GIL;
|
|
PySysPaths = PyUtil::GetSystemPaths();
|
|
}
|
|
for (const FString& PySysPath : PySysPaths)
|
|
{
|
|
const FString PotentialFilePath = PySysPath / TEXT("init_unreal.py");
|
|
if (FPaths::FileExists(PotentialFilePath))
|
|
{
|
|
// Execute these files in the "public" scope, as if their contents had been run directly in the console
|
|
// This allows them to be used to set-up an editor environment for the console
|
|
FPythonCommandEx InitUnrealPythonCommand;
|
|
InitUnrealPythonCommand.FileExecutionScope = EPythonFileExecutionScope::Public;
|
|
RunFile(*PotentialFilePath, *InitUnrealPythonCommand.Command, InitUnrealPythonCommand);
|
|
}
|
|
}
|
|
for (const FString& StartupScript : GetDefault<UPythonScriptPluginSettings>()->StartupScripts)
|
|
{
|
|
ExecPythonCommand(*StartupScript);
|
|
}
|
|
|
|
// Notify any external listeners
|
|
OnPythonInitializedDelegate.Broadcast();
|
|
|
|
#if WITH_EDITOR
|
|
// Activate the Content Browser integration (now that editor subsystems are available)
|
|
if (PythonFileDataSource)
|
|
{
|
|
UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem();
|
|
ContentBrowserData->ActivateDataSource("PythonData");
|
|
}
|
|
|
|
// Register to generate stub code after a short delay
|
|
RequestStubCodeGeneration();
|
|
#endif // WITH_EDITOR
|
|
}
|
|
|
|
RemoteExecution->Tick(InDeltaTime);
|
|
|
|
FPyWrapperTypeReinstancer::Get().ProcessPending();
|
|
}
|
|
|
|
void FPythonScriptPlugin::SyncRemoteExecutionToSettings()
|
|
{
|
|
RemoteExecution->SyncToSettings();
|
|
}
|
|
|
|
void FPythonScriptPlugin::ImportUnrealModule(const TCHAR* InModuleName)
|
|
{
|
|
const FString PythonModuleName = FString::Printf(TEXT("unreal_%s"), InModuleName);
|
|
const FString NativeModuleName = FString::Printf(TEXT("_unreal_%s"), InModuleName);
|
|
|
|
FPyScopedGIL GIL;
|
|
|
|
const TCHAR* ModuleNameToImport = nullptr;
|
|
PyObject* ModuleToReload = nullptr;
|
|
if (PyUtil::IsModuleAvailableForImport(*PythonModuleName, &PyUtil::GetOnDiskUnrealModulesCache()))
|
|
{
|
|
// Python modules that are already loaded should be reloaded if we're requested to import them again
|
|
if (!PyUtil::IsModuleImported(*PythonModuleName, &ModuleToReload))
|
|
{
|
|
ModuleNameToImport = *PythonModuleName;
|
|
}
|
|
}
|
|
else if (PyUtil::IsModuleAvailableForImport(*NativeModuleName, &PyUtil::GetOnDiskUnrealModulesCache()))
|
|
{
|
|
ModuleNameToImport = *NativeModuleName;
|
|
}
|
|
|
|
FPyObjectPtr PyModule;
|
|
if (ModuleToReload)
|
|
{
|
|
PyModule = FPyObjectPtr::StealReference(PyImport_ReloadModule(ModuleToReload));
|
|
}
|
|
else if (ModuleNameToImport)
|
|
{
|
|
PyModule = FPyObjectPtr::StealReference(PyImport_ImportModule(TCHAR_TO_UTF8(ModuleNameToImport)));
|
|
}
|
|
|
|
if (PyModule)
|
|
{
|
|
check(PyUnrealModule);
|
|
PyObject* PyUnrealModuleDict = PyModule_GetDict(PyUnrealModule);
|
|
|
|
// Hoist every public symbol from this module into the top-level "unreal" module
|
|
{
|
|
PyObject* PyModuleDict = PyModule_GetDict(PyModule);
|
|
|
|
PyObject* PyObjKey = nullptr;
|
|
PyObject* PyObjValue = nullptr;
|
|
Py_ssize_t ModuleDictIndex = 0;
|
|
while (PyDict_Next(PyModuleDict, &ModuleDictIndex, &PyObjKey, &PyObjValue))
|
|
{
|
|
if (PyObjKey)
|
|
{
|
|
const FString Key = PyUtil::PyObjectToUEString(PyObjKey);
|
|
if (Key.Len() > 0 && Key[0] != TEXT('_'))
|
|
{
|
|
PyDict_SetItem(PyUnrealModuleDict, PyObjKey, PyObjValue);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
PyUtil::LogPythonError(nullptr, /*bInteractive*/true);
|
|
}
|
|
}
|
|
|
|
PyObject* FPythonScriptPlugin::EvalString(const TCHAR* InStr, const TCHAR* InContext, const int InMode)
|
|
{
|
|
return EvalString(InStr, InContext, InMode, PyConsoleGlobalDict, PyConsoleLocalDict);
|
|
}
|
|
|
|
PyObject* FPythonScriptPlugin::EvalString(const TCHAR* InStr, const TCHAR* InContext, const int InMode, PyObject* InGlobalDict, PyObject* InLocalDict)
|
|
{
|
|
PyCompilerFlags *PyCompFlags = nullptr;
|
|
|
|
PyArena* PyArena = PyArena_New();
|
|
if (!PyArena)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
_mod* PyModule = PyParser_ASTFromString(TCHAR_TO_UTF8(InStr), TCHAR_TO_UTF8(InContext), InMode, PyCompFlags, PyArena);
|
|
if (!PyModule)
|
|
{
|
|
PyArena_Free(PyArena);
|
|
return nullptr;
|
|
}
|
|
|
|
typedef TPyPtr<PyCodeObject> PyCodeObjectPtr;
|
|
PyCodeObjectPtr PyCodeObj = PyCodeObjectPtr::StealReference(PyAST_Compile(PyModule, TCHAR_TO_UTF8(InContext), PyCompFlags, PyArena));
|
|
if (!PyCodeObj)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
return PyEval_EvalCode((PyUtil::FPyCodeObjectType*)PyCodeObj.Get(), InGlobalDict, InLocalDict);
|
|
}
|
|
|
|
bool FPythonScriptPlugin::RunString(FPythonCommandEx& InOutPythonCommand)
|
|
{
|
|
// Execute Python code within this block
|
|
{
|
|
FPyScopedGIL GIL;
|
|
TGuardValue<bool> UnattendedScriptGuard(GIsRunningUnattendedScript, GIsRunningUnattendedScript || EnumHasAnyFlags(InOutPythonCommand.Flags, EPythonCommandFlags::Unattended));
|
|
|
|
int PyExecMode = 0;
|
|
switch (InOutPythonCommand.ExecutionMode)
|
|
{
|
|
case EPythonCommandExecutionMode::ExecuteFile:
|
|
PyExecMode = Py_file_input;
|
|
break;
|
|
case EPythonCommandExecutionMode::ExecuteStatement:
|
|
PyExecMode = Py_single_input;
|
|
break;
|
|
case EPythonCommandExecutionMode::EvaluateStatement:
|
|
PyExecMode = Py_eval_input;
|
|
break;
|
|
default:
|
|
checkf(false, TEXT("Invalid EPythonCommandExecutionMode!"));
|
|
break;
|
|
}
|
|
|
|
FDelegateHandle LogCaptureHandle = PyCore::GetPythonLogCapture().AddLambda([&InOutPythonCommand](EPythonLogOutputType InLogType, const TCHAR* InLogString) { InOutPythonCommand.LogOutput.Add(FPythonLogOutputEntry{ InLogType, InLogString }); });
|
|
FPyObjectPtr PyResult = FPyObjectPtr::StealReference(EvalString(*InOutPythonCommand.Command, TEXT("<string>"), PyExecMode));
|
|
PyCore::GetPythonLogCapture().Remove(LogCaptureHandle);
|
|
|
|
if (PyResult)
|
|
{
|
|
InOutPythonCommand.CommandResult = PyUtil::PyObjectToUEStringRepr(PyResult);
|
|
}
|
|
else if (PyUtil::LogPythonError(&InOutPythonCommand.CommandResult))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
FPyWrapperTypeReinstancer::Get().ProcessPending();
|
|
return true;
|
|
}
|
|
|
|
bool FPythonScriptPlugin::RunFile(const TCHAR* InFile, const TCHAR* InArgs, FPythonCommandEx& InOutPythonCommand)
|
|
{
|
|
auto ResolveFilePath = [InFile]() -> FString
|
|
{
|
|
// Favor the CWD
|
|
if (FPaths::FileExists(InFile))
|
|
{
|
|
return FPaths::ConvertRelativePathToFull(InFile);
|
|
}
|
|
|
|
// Execute Python code within this block
|
|
{
|
|
FPyScopedGIL GIL;
|
|
|
|
// Then test against each system path in order (as Python would)
|
|
const TArray<FString> PySysPaths = PyUtil::GetSystemPaths();
|
|
for (const FString& PySysPath : PySysPaths)
|
|
{
|
|
const FString PotentialFilePath = PySysPath / InFile;
|
|
if (FPaths::FileExists(PotentialFilePath))
|
|
{
|
|
return PotentialFilePath;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Didn't find a match... we know this file doesn't exist, but we'll use this path in the error reporting
|
|
return FPaths::ConvertRelativePathToFull(InFile);
|
|
};
|
|
|
|
const FString ResolvedFilePath = ResolveFilePath();
|
|
|
|
FString FileStr;
|
|
bool bLoaded = FFileHelper::LoadFileToString(FileStr, *ResolvedFilePath);
|
|
#if WITH_EDITOR
|
|
if (CmdMenu)
|
|
{
|
|
CmdMenu->OnRunFile(ResolvedFilePath, bLoaded);
|
|
}
|
|
#endif // WITH_EDITOR
|
|
|
|
if (!bLoaded)
|
|
{
|
|
InOutPythonCommand.CommandResult = FString::Printf(TEXT("Could not load Python file '%s' (resolved from '%s')"), *ResolvedFilePath, InFile);
|
|
UE_LOG(LogPython, Error, TEXT("%s"), *InOutPythonCommand.CommandResult);
|
|
return false;
|
|
}
|
|
|
|
// Execute Python code within this block
|
|
double ElapsedSeconds = 0.0;
|
|
{
|
|
FPyScopedGIL GIL;
|
|
TGuardValue<bool> UnattendedScriptGuard(GIsRunningUnattendedScript, GIsRunningUnattendedScript || EnumHasAnyFlags(InOutPythonCommand.Flags, EPythonCommandFlags::Unattended));
|
|
|
|
FPyObjectPtr PyFileGlobalDict = PyConsoleGlobalDict;
|
|
FPyObjectPtr PyFileLocalDict = PyConsoleLocalDict;
|
|
if (InOutPythonCommand.FileExecutionScope == EPythonFileExecutionScope::Private)
|
|
{
|
|
PyFileGlobalDict = FPyObjectPtr::StealReference(PyDict_Copy(PyDefaultGlobalDict));
|
|
PyFileLocalDict = PyFileGlobalDict;
|
|
}
|
|
{
|
|
FPyObjectPtr PyResolvedFilePath;
|
|
if (PyConversion::Pythonize(ResolvedFilePath, PyResolvedFilePath.Get(), PyConversion::ESetErrorState::No))
|
|
{
|
|
PyDict_SetItemString(PyFileGlobalDict, "__file__", PyResolvedFilePath);
|
|
}
|
|
}
|
|
|
|
FPyObjectPtr PyResult;
|
|
{
|
|
FScopedDurationTimer Timer(ElapsedSeconds);
|
|
FPythonScopedArgv ScopedArgv(InArgs);
|
|
|
|
FDelegateHandle LogCaptureHandle = PyCore::GetPythonLogCapture().AddLambda([&InOutPythonCommand](EPythonLogOutputType InLogType, const TCHAR* InLogString) { InOutPythonCommand.LogOutput.Add(FPythonLogOutputEntry{ InLogType, InLogString }); });
|
|
PyResult = FPyObjectPtr::StealReference(EvalString(*FileStr, *ResolvedFilePath, Py_file_input, PyFileGlobalDict, PyFileLocalDict)); // We can't just use PyRun_File here as Python isn't always built against the same version of the CRT as UE, so we get a crash at the CRT layer
|
|
PyCore::GetPythonLogCapture().Remove(LogCaptureHandle);
|
|
}
|
|
|
|
PyDict_DelItemString(PyFileGlobalDict, "__file__");
|
|
|
|
if (PyResult)
|
|
{
|
|
InOutPythonCommand.CommandResult = PyUtil::PyObjectToUEStringRepr(PyResult);
|
|
}
|
|
else if (PyUtil::LogPythonError(&InOutPythonCommand.CommandResult))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
FPyWrapperTypeReinstancer::Get().ProcessPending();
|
|
|
|
if (FEngineAnalytics::IsAvailable())
|
|
{
|
|
TArray<FAnalyticsEventAttribute> EventAttributes;
|
|
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("Duration"), ElapsedSeconds));
|
|
FEngineAnalytics::GetProvider().RecordEvent(TEXT("PythonScriptPlugin"), EventAttributes);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FPythonScriptPlugin::OnModuleDirtied(FName InModuleName)
|
|
{
|
|
ImportUnrealModule(*InModuleName.ToString());
|
|
}
|
|
|
|
void FPythonScriptPlugin::OnModulesChanged(FName InModuleName, EModuleChangeReason InModuleChangeReason)
|
|
{
|
|
switch (InModuleChangeReason)
|
|
{
|
|
case EModuleChangeReason::ModuleLoaded:
|
|
FPyWrapperTypeRegistry::Get().GenerateWrappedTypesForModule(InModuleName);
|
|
#if WITH_EDITOR
|
|
// Register to generate stub code after a short delay
|
|
RequestStubCodeGeneration();
|
|
#endif // WITH_EDITOR
|
|
break;
|
|
|
|
case EModuleChangeReason::ModuleUnloaded:
|
|
FPyWrapperTypeRegistry::Get().OrphanWrappedTypesForModule(InModuleName);
|
|
#if WITH_EDITOR
|
|
// Register to generate stub code after a short delay
|
|
RequestStubCodeGeneration();
|
|
#endif // WITH_EDITOR
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FPythonScriptPlugin::OnContentPathMounted(const FString& InAssetPath, const FString& InFilesystemPath)
|
|
{
|
|
{
|
|
FPyScopedGIL GIL;
|
|
RegisterModulePaths(InFilesystemPath);
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
if (PythonFileDataSource)
|
|
{
|
|
PythonFileDataSource->AddFileMount(*(InAssetPath / TEXT("Python")), InFilesystemPath / TEXT("Python"));
|
|
}
|
|
#endif // WITH_EDITOR
|
|
}
|
|
|
|
void FPythonScriptPlugin::OnContentPathDismounted(const FString& InAssetPath, const FString& InFilesystemPath)
|
|
{
|
|
{
|
|
FPyScopedGIL GIL;
|
|
UnregisterModulePaths(InFilesystemPath);
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
if (PythonFileDataSource)
|
|
{
|
|
PythonFileDataSource->RemoveFileMount(*(InAssetPath / TEXT("Python")));
|
|
}
|
|
#endif // WITH_EDITOR
|
|
}
|
|
|
|
void FPythonScriptPlugin::RegisterModulePaths(const FString& InFilesystemPath)
|
|
{
|
|
const FString PythonContentPath = FPaths::ConvertRelativePathToFull(InFilesystemPath / TEXT("Python"));
|
|
PyUtil::AddSystemPath(PythonContentPath);
|
|
|
|
const FString PythonContentPlatformSitePackagesPath = PythonContentPath / TEXT("Lib") / FPlatformMisc::GetUBTPlatform() / TEXT("site-packages");
|
|
const FString PythonContentGeneralSitePackagesPath = PythonContentPath / TEXT("Lib") / TEXT("site-packages");
|
|
PyUtil::AddSitePackagesPath(PythonContentPlatformSitePackagesPath);
|
|
PyUtil::AddSitePackagesPath(PythonContentGeneralSitePackagesPath);
|
|
|
|
PyUtil::GetOnDiskUnrealModulesCache().AddModules(*PythonContentPath);
|
|
}
|
|
|
|
void FPythonScriptPlugin::UnregisterModulePaths(const FString& InFilesystemPath)
|
|
{
|
|
const FString PythonContentPath = FPaths::ConvertRelativePathToFull(InFilesystemPath / TEXT("Python"));
|
|
PyUtil::RemoveSystemPath(PythonContentPath);
|
|
|
|
const FString PythonContentPlatformSitePackagesPath = PythonContentPath / TEXT("Lib") / FPlatformMisc::GetUBTPlatform() / TEXT("site-packages");
|
|
const FString PythonContentGeneralSitePackagesPath = PythonContentPath / TEXT("Lib") / TEXT("site-packages");
|
|
PyUtil::RemoveSystemPath(PythonContentPlatformSitePackagesPath);
|
|
PyUtil::RemoveSystemPath(PythonContentGeneralSitePackagesPath);
|
|
|
|
PyUtil::GetOnDiskUnrealModulesCache().RemoveModules(*PythonContentPath);
|
|
}
|
|
|
|
bool FPythonScriptPlugin::IsDeveloperModeEnabled()
|
|
{
|
|
return GetDefault<UPythonScriptPluginSettings>()->bDeveloperMode || GetDefault<UPythonScriptPluginUserSettings>()->bDeveloperMode;
|
|
}
|
|
|
|
void FPythonScriptPlugin::OnAssetRenamed(const FAssetData& Data, const FString& OldName)
|
|
{
|
|
FPyWrapperTypeRegistry& PyWrapperTypeRegistry = FPyWrapperTypeRegistry::Get();
|
|
const FName OldPackageName = *FPackageName::ObjectPathToPackageName(OldName);
|
|
|
|
// If this asset has an associated Python type, then we need to rename it
|
|
if (PyWrapperTypeRegistry.HasWrappedTypeForObjectName(OldPackageName))
|
|
{
|
|
if (const UObject* AssetPtr = PyGenUtil::GetAssetTypeRegistryType(Data.GetAsset()))
|
|
{
|
|
PyWrapperTypeRegistry.UpdateGenerateWrappedTypeForRename(OldPackageName, AssetPtr);
|
|
OnAssetUpdated(AssetPtr);
|
|
}
|
|
else
|
|
{
|
|
PyWrapperTypeRegistry.RemoveGenerateWrappedTypeForDelete(OldPackageName);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FPythonScriptPlugin::OnAssetRemoved(const FAssetData& Data)
|
|
{
|
|
FPyWrapperTypeRegistry& PyWrapperTypeRegistry = FPyWrapperTypeRegistry::Get();
|
|
|
|
// If this asset has an associated Python type, then we need to remove it
|
|
if (PyWrapperTypeRegistry.HasWrappedTypeForObjectName(Data.PackageName))
|
|
{
|
|
PyWrapperTypeRegistry.RemoveGenerateWrappedTypeForDelete(Data.PackageName);
|
|
}
|
|
}
|
|
|
|
void FPythonScriptPlugin::OnAssetReload(const EPackageReloadPhase InPackageReloadPhase, FPackageReloadedEvent* InPackageReloadedEvent)
|
|
{
|
|
if (InPackageReloadPhase == EPackageReloadPhase::PostPackageFixup)
|
|
{
|
|
// Get the primary asset in this package
|
|
// Use the new package as it has the correct name
|
|
const UPackage* NewPackage = InPackageReloadedEvent->GetNewPackage();
|
|
const UObject* NewAsset = StaticFindObject(UObject::StaticClass(), (UPackage*)NewPackage, *FPackageName::GetLongPackageAssetName(NewPackage->GetName()));
|
|
OnAssetUpdated(NewAsset);
|
|
}
|
|
}
|
|
|
|
void FPythonScriptPlugin::OnAssetUpdated(const UObject* InObj)
|
|
{
|
|
if (const UObject* AssetPtr = PyGenUtil::GetAssetTypeRegistryType(InObj))
|
|
{
|
|
// If this asset has an associated Python type, then we need to re-generate it
|
|
FPyWrapperTypeRegistry& PyWrapperTypeRegistry = FPyWrapperTypeRegistry::Get();
|
|
if (PyWrapperTypeRegistry.HasWrappedTypeForObject(AssetPtr))
|
|
{
|
|
FPyWrapperTypeRegistry::FGeneratedWrappedTypeReferences GeneratedWrappedTypeReferences;
|
|
TSet<FName> DirtyModules;
|
|
|
|
PyWrapperTypeRegistry.GenerateWrappedTypeForObject(AssetPtr, GeneratedWrappedTypeReferences, DirtyModules, EPyTypeGenerationFlags::IncludeBlueprintGeneratedTypes | EPyTypeGenerationFlags::OverwriteExisting);
|
|
|
|
PyWrapperTypeRegistry.GenerateWrappedTypesForReferences(GeneratedWrappedTypeReferences, DirtyModules);
|
|
PyWrapperTypeRegistry.NotifyModulesDirtied(DirtyModules);
|
|
}
|
|
}
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
|
|
void FPythonScriptPlugin::OnPrepareToCleanseEditorObject(UObject* InObject)
|
|
{
|
|
FPyReferenceCollector::Get().PurgeUnrealObjectReferences(InObject, true);
|
|
}
|
|
|
|
void FPythonScriptPlugin::PopulatePythonFileContextMenu(UToolMenu* InMenu)
|
|
{
|
|
const UContentBrowserDataMenuContext_FileMenu* ContextObject = InMenu->FindContext<UContentBrowserDataMenuContext_FileMenu>();
|
|
checkf(ContextObject, TEXT("Required context UContentBrowserDataMenuContext_FileMenu was missing!"));
|
|
|
|
if (!PythonFileDataSource)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Extract the internal file paths that belong to this data source from the full list of selected paths given in the context
|
|
TArray<TSharedRef<const FContentBrowserFileItemDataPayload>> SelectedPythonFiles;
|
|
for (const FContentBrowserItem& SelectedItem : ContextObject->SelectedItems)
|
|
{
|
|
if (const FContentBrowserItemData* ItemDataPtr = SelectedItem.GetPrimaryInternalItem())
|
|
{
|
|
if (TSharedPtr<const FContentBrowserFileItemDataPayload> FilePayload = ContentBrowserFileData::GetFileItemPayload(PythonFileDataSource.Get(), *ItemDataPtr))
|
|
{
|
|
SelectedPythonFiles.Add(FilePayload.ToSharedRef());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only add the file items if we have a file path selected
|
|
if (SelectedPythonFiles.Num() > 0)
|
|
{
|
|
// Run
|
|
{
|
|
FToolMenuSection& Section = InMenu->AddSection("PythonScript", LOCTEXT("PythonScriptMenuHeading", "Python Script"));
|
|
Section.InsertPosition.Position = EToolMenuInsertType::First;
|
|
|
|
const FExecuteAction ExecuteRunAction = FExecuteAction::CreateLambda([this, SelectedPythonFiles]()
|
|
{
|
|
for (const TSharedRef<const FContentBrowserFileItemDataPayload>& SelectedPythonFile : SelectedPythonFiles)
|
|
{
|
|
ExecPythonCommand(*SelectedPythonFile->GetFilename());
|
|
}
|
|
});
|
|
|
|
Section.AddMenuEntry(
|
|
"RunPythonScript",
|
|
LOCTEXT("RunPythonScript", "Run..."),
|
|
LOCTEXT("RunPythonScriptToolTip", "Run this script."),
|
|
FSlateIcon(),
|
|
FUIAction(ExecuteRunAction)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif // WITH_EDITOR
|
|
|
|
#endif // WITH_PYTHON
|
|
|
|
IMPLEMENT_MODULE(FPythonScriptPlugin, PythonScriptPlugin)
|
|
|
|
#undef LOCTEXT_NAMESPACE
|