Merge remote-tracking branch 'origin/master' into 1.13

# Conflicts:
#	Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp
This commit is contained in:
2026-08-05 08:12:00 +02:00
38 changed files with 1420 additions and 120 deletions
+1 -1
View File
@@ -80,7 +80,7 @@ Follow the instructions below to compile and run the engine from source.
* Open workspace with XCode or Visual Studio Code
* Build and run (configuration `Editor.Mac.Development`)
#### Troubleshooting
## Troubleshooting
* `Could not execute because the specified command or file was not found.`
+15 -1
View File
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using FlaxEditor.GUI.Tree;
using FlaxEditor.Options;
using FlaxEngine;
@@ -54,6 +54,10 @@ public class TreeViewPanel : Panel
{
Editor.Instance.Windows.ContentWin.Rename(contentNode.Item);
}
else if (node is ContentFolderTreeNode folderNode)
{
Editor.Instance.Windows.ContentWin.Rename(folderNode.Folder);
}
}
}
@@ -102,6 +106,10 @@ public class TreeViewPanel : Panel
{
items.Add(contentNode.Item);
}
else if (node is ContentFolderTreeNode folderNode)
{
items.Add(folderNode.Folder);
}
}
Editor.Instance.Windows.ContentWin.Duplicate(items);
@@ -121,8 +129,12 @@ public class TreeViewPanel : Panel
return;
var filePaths = new List<string>();
foreach (var node in selection)
{
if (node is ContentItemTreeNode contentNode)
filePaths.Add(contentNode.Item.Path);
else if (node is ContentFolderTreeNode folderNode)
filePaths.Add(folderNode.Folder.Path);
}
Clipboard.Files = filePaths.ToArray();
UpdateContentItemCut(false);
@@ -168,6 +180,8 @@ public class TreeViewPanel : Panel
{
if (node is ContentItemTreeNode contentNode)
_cutItems.Add(contentNode.Item);
else if (node is ContentFolderTreeNode folderNode)
_cutItems.Add(folderNode.Folder);
}
}
@@ -330,18 +330,16 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data)
GameCooker::PackageFiles();
// Validate environment variables
Dictionary<String, String> envVars;
Platform::GetEnvironmentVariables(envVars);
String javaHome;
if (!envVars.TryGet(TEXT("JAVA_HOME"), javaHome) || !FileSystem::DirectoryExists(javaHome))
if (Platform::GetEnvironmentVariable(TEXT("JAVA_HOME"), javaHome) || !FileSystem::DirectoryExists(javaHome))
{
LOG(Error, "Missing or invalid JAVA_HOME env variable. {0}", javaHome);
return true;
}
String androidSdk;
if (!envVars.TryGet(TEXT("ANDROID_HOME"), androidSdk) || !FileSystem::DirectoryExists(androidSdk))
if (Platform::GetEnvironmentVariable(TEXT("ANDROID_HOME"), androidSdk) || !FileSystem::DirectoryExists(androidSdk))
{
if (!envVars.TryGet(TEXT("ANDROID_SDK"), androidSdk) || !FileSystem::DirectoryExists(androidSdk))
if (Platform::GetEnvironmentVariable(TEXT("ANDROID_SDK"), androidSdk) || !FileSystem::DirectoryExists(androidSdk))
{
LOG(Error, "Missing or invalid ANDROID_HOME env variable. {0}", androidSdk);
return true;
@@ -355,10 +353,11 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data)
#else
const Char* gradlew = TEXT("gradlew");
#endif
#if PLATFORM_LINUX
#if PLATFORM_LINUX || PLATFORM_MAC
{
CreateProcessSettings procSettings;
procSettings.FileName = String::Format(TEXT("chmod +x \"{0}/gradlew\""), data.OriginalOutputPath);
procSettings.FileName = TEXT("/bin/chmod");
procSettings.Arguments = String::Format(TEXT("+x \"{0}\""), data.OriginalOutputPath / gradlew);
procSettings.WorkingDirectory = data.OriginalOutputPath;
procSettings.HiddenWindow = true;
Platform::CreateProcess(procSettings);
@@ -371,7 +370,8 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data)
// .aab
{
CreateProcessSettings procSettings;
procSettings.FileName = String::Format(TEXT("\"{0}\" {1}"), data.OriginalOutputPath / gradlew, distributionPackage ? TEXT(":app:bundle") : TEXT(":app:bundleDebug"));
procSettings.FileName = data.OriginalOutputPath / gradlew;
procSettings.Arguments = distributionPackage ? TEXT("--console=plain :app:bundle") : TEXT("--console=plain :app:bundleDebug");
procSettings.WorkingDirectory = data.OriginalOutputPath;
const int32 result = Platform::CreateProcess(procSettings);
if (result != 0)
@@ -394,7 +394,8 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data)
// .apk
{
CreateProcessSettings procSettings;
procSettings.FileName = String::Format(TEXT("\"{0}\" {1}"), data.OriginalOutputPath / gradlew, distributionPackage ? TEXT("assemble") : TEXT("assembleDebug"));
procSettings.FileName = data.OriginalOutputPath / gradlew;
procSettings.Arguments = distributionPackage ? TEXT("--console=plain assemble") : TEXT("--console=plain assembleDebug");
procSettings.WorkingDirectory = data.OriginalOutputPath;
const int32 result = Platform::CreateProcess(procSettings);
if (result != 0)
@@ -29,8 +29,8 @@ namespace FlaxEditor.CustomEditors.Dedicated
patchesCount,
patchesCount * 16,
chunkSize,
1.0f / (resolution.X + 1e-9f),
1.0f / (resolution.Z + 1e-9f),
Mathf.Abs(resolution.X),
Mathf.Abs(resolution.Z),
totalSize.X / Units.Meters2Units * 0.001f,
totalSize.Z / Units.Meters2Units * 0.001f
);
@@ -40,6 +40,7 @@ namespace FlaxEditor.Modules.SourceCodeEditing
var codeEditing = Editor.Instance.CodeEditing;
var vsCode = codeEditing.GetInBuildEditor(CodeEditorTypes.VSCode);
var rider = codeEditing.GetInBuildEditor(CodeEditorTypes.Rider);
var clion = codeEditing.GetInBuildEditor(CodeEditorTypes.CLion);
#if PLATFORM_WINDOWS
// Favor the newest Visual Studio
@@ -66,6 +67,8 @@ namespace FlaxEditor.Modules.SourceCodeEditing
_currentEditor = vsCode;
else if (rider != null)
_currentEditor = rider;
else if (clion != null)
_currentEditor = clion;
else
_currentEditor = codeEditing.GetInBuildEditor(CodeEditorTypes.SystemDefault);
}
+26 -4
View File
@@ -811,17 +811,39 @@ namespace FlaxEditor.Modules
private void InitWindowDecorations(RootControl mainWindow)
{
ScriptsBuilder.GetBinariesConfiguration(out _, out _, out _, out var configuration);
string driver = string.Empty;
var tooltip = new System.Text.StringBuilder();
// Project info
tooltip.AppendLine(Editor.GameProject.Name);
var projectPath = Globals.ProjectFolder;
#if PLATFORM_WINDOWS
projectPath = projectPath.Replace('/', '\\');
#endif
tooltip.AppendLine(projectPath);
// Engine info
tooltip.Append("Engine Version: ").AppendLine(Globals.EngineVersion);
var engineNickname = Editor.EngineProject.EngineNickname;
if (!string.IsNullOrEmpty(engineNickname))
tooltip.Append($" ({engineNickname})");
// Build info
#if USE_LARGE_WORLDS
tooltip.AppendLine("Large Worlds Enabled");
#endif
tooltip.Append("Configuration: ").AppendLine(configuration);
tooltip.Append("Graphics: ").Append(GPUDevice.Instance.RendererType);
#if PLATFORM_LINUX
driver = LinuxPlatform.DisplayServer;
var driver = LinuxPlatform.DisplayServer;
if (!string.IsNullOrEmpty(driver))
driver = $" ({driver})";
tooltip.Append($" ({driver})");
#endif
WindowDecorations = new MainWindowDecorations(mainWindow, !Utilities.Utils.UseCustomWindowDecorations(true))
{
Parent = mainWindow,
IconTooltipText = $"{mainWindow.RootWindow.Title}\nVersion {Globals.EngineVersion}\nConfiguration {configuration}\nGraphics {GPUDevice.Instance.RendererType}{driver}",
IconTooltipText = tooltip.ToString(),
};
}
+1 -5
View File
@@ -191,13 +191,9 @@ namespace FlaxEditor.Modules
var mainWindow = MainWindow;
if (mainWindow)
{
var projectPath = Globals.ProjectFolder;
#if PLATFORM_WINDOWS
projectPath = projectPath.Replace('/', '\\');
#endif
var engineVersion = Editor.EngineProject.Version;
var engineVersionText = engineVersion.Revision > 0 ? $"{engineVersion.Major}.{engineVersion.Minor}.{engineVersion.Revision}" : $"{engineVersion.Major}.{engineVersion.Minor}";
var title = $"Flax Editor {engineVersionText} - \'{projectPath}\'";
var title = $"Flax Editor {engineVersionText} - \'{Editor.GameProject.Name}\'";
mainWindow.Title = title;
}
}
+2
View File
@@ -6,6 +6,7 @@
#include "ScriptsBuilder.h"
#include "CodeEditors/VisualStudioCodeEditor.h"
#include "CodeEditors/RiderCodeEditor.h"
#include "CodeEditors/CLionCodeEditor.h"
#if USE_VISUAL_STUDIO_DTE
#include "CodeEditors/VisualStudio/VisualStudioEditor.h"
#endif
@@ -269,6 +270,7 @@ bool CodeEditingManagerService::Init()
#endif
VisualStudioCodeEditor::FindEditors(&CodeEditors);
RiderCodeEditor::FindEditors(&CodeEditors);
CLionCodeEditor::FindEditors(&CodeEditors);
CodeEditors.Add(New<SystemDefaultCodeEditor>());
return false;
+5
View File
@@ -82,6 +82,11 @@ API_ENUM(Namespace="FlaxEditor", Attributes="HideInEditor") enum class CodeEdito
/// </summary>
Rider,
/// <summary>
/// CLion
/// </summary>
CLion,
MAX
};
@@ -0,0 +1,369 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#include "CLionCodeEditor.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Core/Collections/Sorting.h"
#include "Engine/Engine/Globals.h"
#include "Engine/Platform/CreateProcessSettings.h"
#include "Engine/Platform/File.h"
#include "Engine/Serialization/Json.h"
#include "Editor/Editor.h"
#include "Editor/ProjectInfo.h"
#include "Editor/Scripting/ScriptsBuilder.h"
#if PLATFORM_WINDOWS
#include "Engine/Platform/Win32/IncludeWindowsHeaders.h"
#elif PLATFORM_MAC
#include "Engine/Platform/Apple/AppleUtils.h"
#include <AppKit/AppKit.h>
#endif
namespace
{
struct CLionInstallation
{
String path;
String argumentsPrefix;
String version;
CLionInstallation(const String& path_, const String& argumentsPrefix_, const String& version_)
: path(path_)
, argumentsPrefix(argumentsPrefix_)
, version(version_)
{
}
};
void AddInstallation(Array<CLionInstallation*>* installations, String path, const String& version, const String& argumentsPrefix = String::Empty)
{
if (path.IsEmpty())
return;
StringUtils::PathRemoveRelativeParts(path);
for (CLionInstallation* installation : *installations)
{
if (installation->path == path && installation->argumentsPrefix == argumentsPrefix)
return;
}
installations->Add(New<CLionInstallation>(path, argumentsPrefix, version));
}
bool IsCLionProduct(rapidjson_flax::Document& document)
{
auto productCodeMember = document.FindMember("productCode");
if (productCodeMember != document.MemberEnd() && productCodeMember->value == "CL")
return true;
auto nameMember = document.FindMember("name");
if (nameMember == document.MemberEnd())
return false;
const String name = nameMember->value.GetText();
return name == TEXT("CLion") || name == TEXT("JetBrains CLion");
}
void SearchDirectory(Array<CLionInstallation*>* installations, const String& directory, String launchOverridePath = String::Empty, String launchArgumentsPrefix = String::Empty)
{
if (!FileSystem::DirectoryExists(directory))
return;
// Load product info
Array<byte> productInfoData;
const String productInfoPath = directory / TEXT("product-info.json");
if (!FileSystem::FileExists(productInfoPath) || File::ReadAllBytes(productInfoPath, productInfoData))
return;
rapidjson_flax::Document document;
document.Parse((char*)productInfoData.Get(), productInfoData.Count());
if (document.HasParseError() || !IsCLionProduct(document))
return;
// Find version
auto versionMember = document.FindMember("version");
if (versionMember == document.MemberEnd())
return;
// Find executable file path
auto launchMember = document.FindMember("launch");
if (launchMember == document.MemberEnd() || !launchMember->value.IsArray() || launchMember->value.Size() == 0)
return;
auto launcherPathMember = launchMember->value[0].FindMember("launcherPath");
if (launcherPathMember == launchMember->value[0].MemberEnd())
return;
auto launcherPath = launcherPathMember->value.GetText();
auto exePath = directory / launcherPath;
if (!launcherPath.HasChars() || !FileSystem::FileExists(exePath))
return;
AddInstallation(installations, launchOverridePath != String::Empty ? launchOverridePath : exePath, versionMember->value.GetText(), launchArgumentsPrefix);
}
void SearchExecutable(Array<CLionInstallation*>* installations, const String& path)
{
if (FileSystem::FileExists(path))
AddInstallation(installations, path, TEXT("0.0.0"));
}
void ParseVersion(const String& text, int32 version[3])
{
version[0] = 0;
version[1] = 0;
version[2] = 0;
Array<String> values;
text.Split('.', values);
for (int32 i = 0; i < values.Count() && i < 3; i++)
StringUtils::Parse(values[i].Get(), &version[i]);
}
#if PLATFORM_WINDOWS
bool FindRegistryKeyItems(HKEY hKey, Array<String>& results)
{
Char nameBuffer[256];
for (int32 i = 0;; i++)
{
const LONG result = RegEnumKeyW(hKey, i, nameBuffer, ARRAY_COUNT(nameBuffer));
if (result == ERROR_NO_MORE_ITEMS)
break;
if (result != ERROR_SUCCESS)
return false;
results.Add(nameBuffer);
}
return true;
}
void SearchRegistry(Array<CLionInstallation*>* installations, HKEY root, const Char* key, const Char* valueName = TEXT(""))
{
// Open key
HKEY keyH;
if (RegOpenKeyExW(root, key, 0, KEY_READ, &keyH) != ERROR_SUCCESS)
return;
// Iterate over subkeys
Array<String> subKeys;
if (FindRegistryKeyItems(keyH, subKeys))
{
for (auto& subKey : subKeys)
{
HKEY subKeyH;
if (RegOpenKeyExW(keyH, *subKey, 0, KEY_READ, &subKeyH) != ERROR_SUCCESS)
continue;
// Read subkey value
DWORD type;
DWORD cbData;
if (RegQueryValueExW(subKeyH, valueName, nullptr, &type, nullptr, &cbData) != ERROR_SUCCESS || type != REG_SZ)
{
RegCloseKey(subKeyH);
continue;
}
Array<Char> data;
data.Resize((int32)cbData / sizeof(Char));
if (RegQueryValueExW(subKeyH, valueName, nullptr, nullptr, reinterpret_cast<LPBYTE>(data.Get()), &cbData) != ERROR_SUCCESS)
{
RegCloseKey(subKeyH);
continue;
}
// Check if it's a valid installation path
String path(data.Get(), data.Count() - 1);
SearchDirectory(installations, path);
RegCloseKey(subKeyH);
}
}
RegCloseKey(keyH);
}
#endif
}
bool SortInstallations(CLionInstallation* const& i1, CLionInstallation* const& i2)
{
int32 version1[3], version2[3];
ParseVersion(i1->version, version1);
ParseVersion(i2->version, version2);
// Compare by MAJOR.MINOR.BUILD
if (version1[0] == version2[0])
{
if (version1[1] == version2[1])
return version1[2] > version2[2];
return version1[1] > version2[1];
}
return version1[0] > version2[0];
}
CLionCodeEditor::CLionCodeEditor(const String& execPath, const String& execArgsPrefix)
: _execPath(execPath)
, _execArgsPrefix(execArgsPrefix)
, _projectPath(Globals::ProjectFolder / TEXT("Cache/Projects/CMake") / Editor::Project->Name)
{
}
String CLionCodeEditor::GetProcessArguments(const String& arguments) const
{
return _execArgsPrefix.HasChars() ? _execArgsPrefix + TEXT(" ") + arguments : arguments;
}
void CLionCodeEditor::FindEditors(Array<CodeEditor*>* output)
{
Array<CLionInstallation*> installations;
Array<String> subDirectories;
String localAppDataPath;
FileSystem::GetSpecialFolderPath(SpecialFolder::LocalAppData, localAppDataPath);
#if PLATFORM_WINDOWS
// Lookup from all known registry locations
SearchRegistry(&installations, HKEY_CURRENT_USER, TEXT("SOFTWARE\\JetBrains\\CLion"));
SearchRegistry(&installations, HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\JetBrains\\CLion"));
SearchRegistry(&installations, HKEY_CURRENT_USER, TEXT("SOFTWARE\\JetBrains\\CLion"), TEXT("InstallDir"));
SearchRegistry(&installations, HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\JetBrains\\CLion"), TEXT("InstallDir"));
SearchRegistry(&installations, HKEY_CURRENT_USER, TEXT("SOFTWARE\\WOW6432Node\\JetBrains\\CLion"));
SearchRegistry(&installations, HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\WOW6432Node\\JetBrains\\CLion"));
// Versions installed via JetBrains Toolbox
FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("Programs"));
FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains\\Toolbox\\apps\\CLion\\ch-0\\"));
FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains\\Toolbox\\apps\\CLion\\ch-1\\")); // Beta versions
#endif
#if PLATFORM_LINUX
// TODO: detect Snap installations by reading the desktop file from ~/.local/share/applications and /usr/share/applications.
SearchDirectory(&installations, TEXT("/usr/share/clion/"));
FileSystem::GetChildDirectories(subDirectories, TEXT("/usr/share/clion"));
// Default suggested location for standalone installations
FileSystem::GetChildDirectories(subDirectories, TEXT("/opt/"));
// Versions installed via JetBrains Toolbox
SearchDirectory(&installations, localAppDataPath / TEXT("JetBrains/Toolbox/apps/clion/"));
FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains/Toolbox/apps/CLion/ch-0"));
FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains/Toolbox/apps/CLion/ch-1")); // Beta versions
// Detect Flatpak installations
SearchDirectory(&installations,
TEXT("/var/lib/flatpak/app/com.jetbrains.CLion/current/active/files/extra/clion/"),
TEXT("flatpak"),
TEXT("run com.jetbrains.CLion"));
SearchExecutable(&installations, TEXT("/usr/bin/clion"));
SearchExecutable(&installations, TEXT("/snap/bin/clion"));
#endif
#if PLATFORM_MAC
String applicationSupportFolder;
FileSystem::GetSpecialFolderPath(SpecialFolder::ProgramData, applicationSupportFolder);
NSURL* appURL = [[NSWorkspace sharedWorkspace] URLForApplicationWithBundleIdentifier:@"com.jetbrains.CLion"];
if (appURL != nullptr)
{
const String appPath = AppleUtils::ToString((CFStringRef)[appURL path]);
SearchDirectory(&installations, appPath / TEXT("Contents/Resources"), appPath);
}
Array<String> subMacDirectories;
FileSystem::GetChildDirectories(subMacDirectories, applicationSupportFolder / TEXT("JetBrains/Toolbox/apps/CLion/ch-0/"));
FileSystem::GetChildDirectories(subMacDirectories, applicationSupportFolder / TEXT("JetBrains/Toolbox/apps/CLion/ch-1/"));
for (const String& directory : subMacDirectories)
{
String clionAppPath = directory / TEXT("CLion.app");
SearchDirectory(&installations, clionAppPath / TEXT("Contents/Resources"), clionAppPath);
}
// Check the local installer version
SearchDirectory(&installations, TEXT("/Applications/CLion.app/Contents/Resources"), TEXT("/Applications/CLion.app"));
String userFolder;
FileSystem::GetSpecialFolderPath(SpecialFolder::Documents, userFolder);
String clionAppPath = userFolder / TEXT("../Applications/CLion.app");
SearchDirectory(&installations, clionAppPath / TEXT("Contents/Resources"), clionAppPath);
#endif
for (const String& directory : subDirectories)
SearchDirectory(&installations, directory);
// Sort found installations by version number
Sorting::QuickSort(installations.Get(), installations.Count(), &SortInstallations);
for (CLionInstallation* installation : installations)
{
output->Add(New<CLionCodeEditor>(installation->path, installation->argumentsPrefix));
Delete(installation);
}
}
CodeEditorTypes CLionCodeEditor::GetType() const
{
return CodeEditorTypes::CLion;
}
String CLionCodeEditor::GetName() const
{
return TEXT("CLion");
}
String CLionCodeEditor::GetGenerateProjectCustomArgs() const
{
return TEXT("-clion");
}
void CLionCodeEditor::OpenFile(const String& path, int32 line)
{
// Generate project files if missing
if (!FileSystem::FileExists(_projectPath / TEXT("CMakeLists.txt")) ||
!FileSystem::FileExists(_projectPath / TEXT("CMakePresets.json")))
{
ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs());
}
// Open file
line = line > 0 ? line : 1;
CreateProcessSettings procSettings;
#if !PLATFORM_MAC
procSettings.FileName = _execPath;
procSettings.Arguments = GetProcessArguments(String::Format(TEXT("\"{0}\" --line {2} \"{1}\""), _projectPath, path, line));
#else
procSettings.FileName = "/usr/bin/open";
procSettings.Arguments = String::Format(TEXT("-n -a \"{0}\" --args \"{1}\" --line {3} \"{2}\""), _execPath, _projectPath, path, line);
#endif
procSettings.HiddenWindow = false;
procSettings.WaitForEnd = false;
procSettings.LogOutput = false;
procSettings.ShellExecute = true;
Platform::CreateProcess(procSettings);
}
void CLionCodeEditor::OpenSolution()
{
// Generate project files if missing
if (!FileSystem::FileExists(_projectPath / TEXT("CMakeLists.txt")) ||
!FileSystem::FileExists(_projectPath / TEXT("CMakePresets.json")))
{
ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs());
}
// Open solution
CreateProcessSettings procSettings;
#if !PLATFORM_MAC
procSettings.FileName = _execPath;
procSettings.Arguments = GetProcessArguments(String::Format(TEXT("\"{0}\""), _projectPath));
#else
procSettings.FileName = "/usr/bin/open";
procSettings.Arguments = String::Format(TEXT("-n -a \"{0}\" \"{1}\""), _execPath, _projectPath);
#endif
procSettings.HiddenWindow = false;
procSettings.WaitForEnd = false;
procSettings.LogOutput = false;
procSettings.ShellExecute = true;
Platform::CreateProcess(procSettings);
}
void CLionCodeEditor::OnFileAdded(const String& path)
{
ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs());
}
@@ -0,0 +1,46 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#pragma once
#include "Editor/Scripting/CodeEditor.h"
/// <summary>
/// Implementation of code editor utility that is using CLion from JetBrains.
/// </summary>
class CLionCodeEditor : public CodeEditor
{
private:
String _execPath;
String _execArgsPrefix;
String _projectPath;
String GetProcessArguments(const String& arguments) const;
public:
/// <summary>
/// Initializes a new instance of the <see cref="CLionCodeEditor"/> class.
/// </summary>
/// <param name="execPath">Executable file path</param>
/// <param name="execArgsPrefix">Additional arguments to pass before CLion arguments.</param>
CLionCodeEditor(const String& execPath, const String& execArgsPrefix = String::Empty);
public:
/// <summary>
/// Tries to find installed CLion instances. Adds them to the result list.
/// </summary>
/// <param name="output">The output editors.</param>
static void FindEditors(Array<CodeEditor*>* output);
public:
// [CodeEditor]
CodeEditorTypes GetType() const override;
String GetName() const override;
String GetGenerateProjectCustomArgs() const override;
void OpenFile(const String& path, int32 line) override;
void OpenSolution() override;
void OnFileAdded(const String& path) override;
};
@@ -431,7 +431,7 @@ namespace FlaxEditor.Viewport.Previews
{
base.Draw();
if (_showCurrentLOD)
if (_showCurrentLOD && Model)
{
var asset = Model;
var lodIndex = ComputeLODIndex(asset, out var screenSize);
+6 -5
View File
@@ -14,6 +14,7 @@ namespace FlaxEditor.Content.Settings
internal const string PS5PlatformSettingsTypename = "FlaxEditor.Content.Settings.PS5PlatformSettings";
internal const string XboxOnePlatformSettingsTypename = "FlaxEditor.Content.Settings.XboxOnePlatformSettings";
internal const string XboxScarlettPlatformSettingsTypename = "FlaxEditor.Content.Settings.XboxScarlettPlatformSettings";
internal const string AndroidPlatformSettingsTypename = "FlaxEditor.Content.Settings.AndroidPlatformSettings";
internal const string SwitchPlatformSettingsTypename = "FlaxEditor.Content.Settings.SwitchPlatformSettings";
#if FLAX_EDITOR
internal static string[] OptionalPlatformSettings =
@@ -172,9 +173,9 @@ namespace FlaxEditor.Content.Settings
#if FLAX_EDITOR || PLATFORM_ANDROID
/// <summary>
/// Reference to <see cref="AndroidPlatformSettings"/> asset. Used to apply configuration on Android platform.
/// Reference to Android Platform Settings asset. Used to apply configuration on Android platform.
/// </summary>
[EditorOrder(2060), EditorDisplay("Platform Settings", "Android"), AssetReference(typeof(AndroidPlatformSettings), true), Tooltip("Reference to Android Platform Settings asset")]
[EditorOrder(2060), EditorDisplay("Platform Settings", "Android"), AssetReference(AndroidPlatformSettingsTypename, true), Tooltip("Reference to Android Platform Settings asset")]
public JsonAsset AndroidPlatform;
#endif
@@ -334,8 +335,8 @@ namespace FlaxEditor.Content.Settings
return Load(gameSettings.XboxScarlettPlatform, XboxScarlettPlatformSettingsTypename) as T;
#endif
#if FLAX_EDITOR || PLATFORM_ANDROID
if (type == typeof(AndroidPlatformSettings))
return Load<AndroidPlatformSettings>(gameSettings.AndroidPlatform) as T;
if (type.FullName == AndroidPlatformSettingsTypename)
return Load(gameSettings.AndroidPlatform, AndroidPlatformSettingsTypename) as T;
#endif
#if FLAX_EDITOR || PLATFORM_SWITCH
if (type.FullName == SwitchPlatformSettingsTypename)
@@ -436,7 +437,7 @@ namespace FlaxEditor.Content.Settings
return gameSettings.XboxScarlettPlatform;
#endif
#if FLAX_EDITOR || PLATFORM_ANDROID
if (type == typeof(AndroidPlatformSettings))
if (type.FullName == AndroidPlatformSettingsTypename)
return gameSettings.AndroidPlatform;
#endif
#if FLAX_EDITOR || PLATFORM_SWITCH
@@ -27,22 +27,30 @@ void BackBufferVulkan::Setup(GPUSwapChainVulkan* window, VkImage backbuffer, Pix
ImageAcquiredSemaphore = New<SemaphoreVulkan>(Device);
}
void BackBufferVulkan::WaitForSubmit()
{
if (SubmitCmdBuffer)
{
if (SubmitCmdBufferFenceCounter == SubmitCmdBuffer->GetFenceSignaledCounter())
SubmitCmdBuffer->Wait();
SubmitCmdBuffer = nullptr;
SubmitCmdBufferFenceCounter = 0;
}
}
void BackBufferVulkan::Release()
{
WaitForSubmit();
Handle.Release();
Delete(RenderingDoneSemaphore);
Delete(ImageAcquiredSemaphore);
if (SubmitCmdBuffer)
{
SubmitCmdBuffer->Wait();
SubmitCmdBuffer = nullptr;
}
Device = nullptr;
}
GPUSwapChainVulkan::GPUSwapChainVulkan(GPUDeviceVulkan* device, Window* window)
: GPUResourceVulkan(device, StringView::Empty)
, _surface(VK_NULL_HANDLE)
, _surfaceWindowHandle(nullptr)
, _swapChain(VK_NULL_HANDLE)
, _currentImageIndex(-1)
, _semaphoreIndex(0)
@@ -61,16 +69,17 @@ void GPUSwapChainVulkan::ReleaseBackBuffer()
_backBuffers.Clear();
}
void GPUSwapChainVulkan::OnReleaseGPU()
void GPUSwapChainVulkan::ReleaseSwapChain(bool releaseSurface)
{
GPUDeviceLock lock(_device);
_device->WaitForGPU();
// The caller must ensure GPU work using the current swapchain is complete before destroying it.
if (_memoryUsage != 0)
{
PROFILE_MEM_DEC(Graphics, _memoryUsage);
_memoryUsage = 0;
}
ReleaseBackBuffer();
// Release data
PROFILE_MEM_DEC(Graphics, _memoryUsage);
_currentImageIndex = -1;
_semaphoreIndex = 0;
_acquiredImageIndex = -1;
@@ -80,13 +89,23 @@ void GPUSwapChainVulkan::OnReleaseGPU()
vkDestroySwapchainKHR(_device->Device, _swapChain, nullptr);
_swapChain = VK_NULL_HANDLE;
}
if (_surface != VK_NULL_HANDLE)
// Resize only invalidates the swapchain. Destroy the native surface only if it is no longer valid
// or when the whole GPU resource is being released.
if (releaseSurface && _surface != VK_NULL_HANDLE)
{
vkDestroySurfaceKHR(GPUDeviceVulkan::Instance, _surface, nullptr);
_surface = VK_NULL_HANDLE;
_surfaceWindowHandle = nullptr;
}
_width = _height = 0;
_memoryUsage = 0;
}
void GPUSwapChainVulkan::OnReleaseGPU()
{
GPUDeviceLock lock(_device);
_device->WaitForGPU();
ReleaseSwapChain(true);
}
bool GPUSwapChainVulkan::IsFullscreen()
@@ -116,24 +135,34 @@ GPUTextureView* GPUSwapChainVulkan::GetBackBufferView()
if (_acquiredImageIndex == -1)
{
PROFILE_CPU();
auto context = _device->MainContext;
auto cmdBufferManager = context->GetCmdBufferManager();
// Keep commands recorded before acquire independent from the image-acquired semaphore wait below.
if (cmdBufferManager->HasPendingActiveCmdBuffer())
context->Flush();
if (TryPresent(DoAcquireImageIndex) < 0)
{
LOG(Fatal, "Swapchain acquire image index failed!");
}
ASSERT(_acquiredImageIndex != -1);
auto context = _device->MainContext;
// Wait for prior GPU work that used this acquired image before recording
// commands against it again. Waiting before acquire can target a different image
// and unnecessarily serialize frames when the swapchain has multiple images.
auto& acquiredBackBuffer = _backBuffers[_acquiredImageIndex];
acquiredBackBuffer.WaitForSubmit();
const auto backBuffer = &_backBuffers[_acquiredImageIndex].Handle;
auto cmdBufferManager = context->GetCmdBufferManager();
auto cmdBuffer = cmdBufferManager->GetCmdBuffer();
// Transition to render target (typical usage in most cases when calling backbuffer getter)
context->AddImageBarrier(backBuffer, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
context->FlushBarriers();
// Submit here so we can add a dependency with the acquired semaphore
cmdBuffer->AddWaitSemaphore(VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, _acquiredSemaphore);
// Wait until the presentation engine releases the image before the first color-output work can use it.
cmdBuffer->AddWaitSemaphore(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, _acquiredSemaphore);
cmdBufferManager->SubmitActiveCmdBuffer();
cmdBufferManager->GetNewActiveCommandBuffer();
ASSERT(cmdBufferManager->HasPendingActiveCmdBuffer() && cmdBufferManager->GetActiveCmdBuffer()->GetState() == CmdBufferVulkan::State::IsInsideBegin);
@@ -145,17 +174,6 @@ void GPUSwapChainVulkan::Begin(RenderTask* task)
{
GPUSwapChain::Begin(task);
// Wait for the backbuffer to be available
if (_currentImageIndex != -1)
{
auto& backBuffer = _backBuffers[_currentImageIndex];
if (backBuffer.SubmitCmdBuffer)
{
backBuffer.SubmitCmdBuffer->Wait();
backBuffer.SubmitCmdBuffer = nullptr;
}
}
// Rebuild swapchain if need to
if (_vsyncPending != _vsyncCurrent)
{
@@ -171,8 +189,9 @@ bool GPUSwapChainVulkan::Resize(int32 width, int32 height)
if (width == _width && height == _height)
return false;
// Wait for GPU to flush commands
_device->WaitForGPU();
// Flush any pending commands referencing the previous backbuffer before waiting on the device.
if (_swapChain != VK_NULL_HANDLE)
_device->GetMainContext()->Flush();
return CreateSwapChain(width, height);
}
@@ -204,7 +223,7 @@ void GPUSwapChainVulkan::CopyBackbuffer(GPUContext* context, GPUTexture* dst)
vkCmdCopyImage(contextVulkan->GetCmdBufferManager()->GetCmdBuffer()->GetHandle(), backBuffer->Image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstVulkan->GetHandle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
}
bool GPUSwapChainVulkan::CreateSwapChain(int32 width, int32 height)
bool GPUSwapChainVulkan::CreateSwapChain(int32 width, int32 height, bool recreateSurface)
{
// Skip if window handle is missing (eg. Android window is not yet visible)
auto windowHandle = _window->GetNativePtr();
@@ -213,14 +232,20 @@ bool GPUSwapChainVulkan::CreateSwapChain(int32 width, int32 height)
PROFILE_CPU();
GPUDeviceLock lock(_device);
const auto device = _device->Device;
// Check if surface has been created before
if (_surface != VK_NULL_HANDLE)
const bool hasSwapChain = _swapChain != VK_NULL_HANDLE;
if (_surface != VK_NULL_HANDLE && _surfaceWindowHandle != windowHandle)
{
// Release previous data
ReleaseGPU();
// Android can replace ANativeWindow during lifecycle changes; iOS can replace the UIView/CAMetalLayer.
// A VkSurfaceKHR must not outlive the native object it was created from.
recreateSurface = true;
}
// Flush removed resources
if (recreateSurface || hasSwapChain)
{
// Retire old swapchain resources before creating replacement images. On plain resize the VkSurfaceKHR
// is intentionally reused; recreate it only when Vulkan or the platform window lifetime requires it.
_device->WaitForGPU();
ReleaseSwapChain(recreateSurface);
_device->DeferredDeletionQueue.ReleaseResources(true);
}
if (_vsyncInit)
@@ -231,15 +256,19 @@ bool GPUSwapChainVulkan::CreateSwapChain(int32 width, int32 height)
_vsyncPending &= _window == Engine::MainWindow; // Don't use VSync on new context menus or tooltips in Editor
_vsyncInit = false;
}
ASSERT(_surface == VK_NULL_HANDLE);
ASSERT(!recreateSurface || _surface == VK_NULL_HANDLE);
ASSERT_LOW_LAYER(_backBuffers.Count() == 0);
// Create platform-dependent surface
VulkanPlatform::CreateSurface(_window, _device, GPUDeviceVulkan::Instance, &_surface);
// Create platform-dependent surface if this is the first swapchain or if the previous surface was lost.
if (_surface == VK_NULL_HANDLE)
{
LOG(Warning, "Failed to create Vulkan surface.");
return true;
VulkanPlatform::CreateSurface(_window, _device, GPUDeviceVulkan::Instance, &_surface);
if (_surface == VK_NULL_HANDLE)
{
LOG(Warning, "Failed to create Vulkan surface.");
return true;
}
_surfaceWindowHandle = windowHandle;
}
_memoryUsage = 1;
@@ -542,8 +571,8 @@ int32 GPUSwapChainVulkan::TryPresent(Function<int32(GPUSwapChainVulkan*, void*)>
// Recreate swapchain
ASSERT(_swapChain != VK_NULL_HANDLE);
int32 width = _width, height = _height;
ReleaseGPU();
CreateSwapChain(width, height);
// Preserve the surface for regular out-of-date swaps; recreate it only when Vulkan reports loss.
CreateSwapChain(width, height, status == (int32)Status::LostSurface);
// Flush commands
_device->GetMainContext()->Flush();
@@ -617,6 +646,7 @@ void GPUSwapChainVulkan::Present(bool vsync)
acquiredBackBuffer.SubmitCmdBuffer = context->GetCmdBufferManager()->GetActiveCmdBuffer();
context->GetCmdBufferManager()->SubmitActiveCmdBuffer(_backBuffers[_acquiredImageIndex].RenderingDoneSemaphore);
acquiredBackBuffer.SubmitCmdBufferFenceCounter = acquiredBackBuffer.SubmitCmdBuffer->GetSubmittedFenceCounter();
// Present the back buffer to the viewport window
const auto result = TryPresent(DoPresent, _device->PresentQueue, true);
@@ -630,7 +660,6 @@ void GPUSwapChainVulkan::Present(bool vsync)
// Rebuild swapchain for the next present
int32 width = _width, height = _height;
ReleaseGPU();
CreateSwapChain(width, height);
_device->GetMainContext()->Flush();
_device->WaitForGPU();
@@ -34,6 +34,11 @@ public:
/// </summary>
CmdBufferVulkan* SubmitCmdBuffer = nullptr;
/// <summary>
/// The fence counter value for SubmitCmdBuffer at the time it was submitted.
/// </summary>
uint64 SubmitCmdBufferFenceCounter = 0;
/// <summary>
/// The render target surface handle.
/// </summary>
@@ -41,6 +46,7 @@ public:
public:
void Setup(GPUSwapChainVulkan* window, VkImage backbuffer, PixelFormat format, VkExtent3D extent);
void WaitForSubmit();
void Release();
public:
@@ -61,6 +67,7 @@ class GPUSwapChainVulkan : public GPUResourceVulkan<GPUSwapChain>, public Resour
private:
VkSurfaceKHR _surface;
void* _surfaceWindowHandle;
VkSwapchainKHR _swapChain;
int32 _currentImageIndex;
int32 _semaphoreIndex;
@@ -106,7 +113,11 @@ public:
private:
void ReleaseBackBuffer();
bool CreateSwapChain(int32 width, int32 height);
// Releases swapchain-owned images and synchronization state. Keeps the VkSurfaceKHR alive during
// normal resize so the native surface is rebuilt only after surface loss or native window replacement.
void ReleaseSwapChain(bool releaseSurface);
// Recreates the swapchain for the requested size. Set recreateSurface when the VkSurfaceKHR is no longer valid.
bool CreateSwapChain(int32 width, int32 height, bool recreateSurface = false);
public:
// [GPUSwapChain]
@@ -956,7 +956,7 @@ void AndroidPlatform::Tick()
// Pool app events
int events;
android_poll_source* source;
while (ALooper_pollAll(0, nullptr, &events, reinterpret_cast<void**>(&source)) >= 0)
while (ALooper_pollOnce(0, nullptr, &events, reinterpret_cast<void**>(&source)) >= 0)
{
// Process event
if (source != nullptr)
@@ -88,6 +88,20 @@ const char* StringUtils::FindIgnoreCase(const char* str, const char* toFind)
return nullptr;
}
void StringUtils::ConvertUTF162ASCII(const Char* from, char* to, int32 len)
{
if (!from || !to)
return;
for (int32 i = 0; i < len; i++)
{
Char c = from[i];
if ((c == 0x09 || c == 0x0A || c == 0x0D || (0x20 <= c && c <= 0x7F)))
to[i] = (char)c;
else
to[i] = ' ';
}
}
void PrintUTF8Error(const char* from, uint32 fromLength)
{
LOG(Error, "Not a UTF-8 string. Length: {0}", fromLength);
@@ -471,6 +471,13 @@ int32 MacPlatform::CreateProcess(CreateProcessSettings& settings)
task.arguments = AppleUtils::ParseArguments(AppleUtils::ToNSString(settings.Arguments));
if (settings.WorkingDirectory.HasChars())
task.currentDirectoryPath = AppleUtils::ToNSString(settings.WorkingDirectory);
[task setStandardInput:[NSFileHandle fileHandleWithNullDevice]];
if (!captureStdOut)
{
NSFileHandle* nullDevice = [NSFileHandle fileHandleWithNullDevice];
[task setStandardOutput:nullDevice];
[task setStandardError:nullDevice];
}
int32 returnCode = 0;
if (settings.WaitForEnd)
+3
View File
@@ -150,6 +150,9 @@ public:
// Converts characters from UTF-16 to ANSI
static void ConvertUTF162ANSI(const Char* from, char* to, int32 len);
// Converts characters from UTF-16 to ASCII
static void ConvertUTF162ASCII(const Char* from, char* to, int32 len);
// Convert characters from UTF-8 to UTF-16
static void ConvertUTF82UTF16(const char* from, Char* to, int32 fromLength, int32& toLength);
+7 -1
View File
@@ -350,7 +350,13 @@ MessagePipeline MainThreadPipeline;
// Create UI thread update callback
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(UIThreadMain)];
self.displayLink.preferredFramesPerSecond = 60;
const int32 targetFrameRate = 60;
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000
if (@available(iOS 15.0, *))
self.displayLink.preferredFrameRateRange = CAFrameRateRangeMake(targetFrameRate, targetFrameRate, targetFrameRate);
else
#endif
self.displayLink.preferredFramesPerSecond = targetFrameRate;
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
// Run engine on a separate main thread
@@ -140,7 +140,9 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value)
// Write code
_writer.Write(TEXT("{\n"));
_writer.Write(*code);
_writer.Write(code);
if (!code.EndsWith(TEXT("\n")) && !code.EndsWith(TEXT(" ")))
_writer.Write(TEXT("\n"));
_writer.Write(TEXT("}\n"));
// Link output values to boxes
@@ -627,8 +627,9 @@ bool MaterialGenerator::Generate(WriteStream& source, MaterialInfo& materialInfo
if (in.Length() > 0)
{
tmp.EnsureCapacity(in.Length() + 1, false);
StringUtils::ConvertUTF162ANSI(*in, tmp.Get(), in.Length());
StringUtils::ConvertUTF162ASCII(*in, tmp.Get(), in.Length());
source.WriteBytes(tmp.Get(), in.Length());
tmp.Clear();
}
}
}
+3 -3
View File
@@ -1309,10 +1309,10 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
}
break;
case ModelType::SkinnedModel:
if (data.LODs.Count() > 1)
if (data.LODs.IsEmpty() || data.LODs[0].Meshes.IsEmpty())
{
LOG(Warning, "Imported skinned model has more than one LOD. Removing the lower LODs. Only single one is supported.");
data.LODs.Resize(1);
errorMsg = TEXT("Imported skinned model has no valid geometry.");
return true;
}
break;
case ModelType::Animation:
@@ -378,7 +378,7 @@ namespace FlaxEngine.GUI
{
var leftEdge = selection.StartIndex <= textBlock.Range.StartIndex ? textBlock.Bounds.UpperLeft : GetCharPosition(selection.StartIndex, out _);
var rightEdge = selection.EndIndex >= textBlock.Range.EndIndex ? textBlock.Bounds.UpperRight : GetCharPosition(selection.EndIndex, out _);
float height = font.Height;
float height = font.Height / DpiScale;
#if PLATFORM_MAC && !PLATFORM_SDL
height /= (float)Platform.Dpi / 96.0f; // TODO: refactor DPI support on macOS to skip such hacks
#endif
@@ -431,7 +431,7 @@ namespace FlaxEngine.GUI
if (textBlock.Style.UnderlineBrush != null)
{
var underLineHeight = 2.0f;
var height = font.Height;
var height = font.Height / DpiScale;
var underlineRect = new Rectangle(textBlock.Bounds.Location.X, textBlock.Bounds.Location.Y + height - underLineHeight * 0.5f, textBlock.Bounds.Width, underLineHeight);
textBlock.Style.UnderlineBrush.Draw(underlineRect, textBlock.Style.Color);
}
+134 -28
View File
@@ -1,6 +1,7 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
namespace FlaxEngine.GUI;
@@ -101,6 +102,7 @@ public class Slider : ContainerControl
private Float2 _thumbSize = new Float2(16, 16);
private bool _isSliding;
private bool _mouseOverThumb;
private const float _step = 10;
/// <summary>
/// Gets or sets the value (normalized to range 0-100).
@@ -340,6 +342,13 @@ public class Slider : ContainerControl
}
}
private void StartSliding()
{
_isSliding = true;
StartMouseCapture();
SlidingStart?.Invoke();
}
private void EndSliding()
{
_isSliding = false;
@@ -409,38 +418,84 @@ public class Slider : ContainerControl
}
/// <inheritdoc />
public override bool OnMouseDown(Float2 location, MouseButton button)
public override Control OnNavigate(NavDirection direction, Float2 location, Control caller, List<Control> visited)
{
if (button == MouseButton.Left)
{
Focus();
float mousePosition = Direction is SliderDirection.HorizontalRight or SliderDirection.HorizontalLeft ? location.X : location.Y;
// Auto-focus self when navigation comes in
if (!IsNavFocused)
return this;
if (_thumbRect.Contains(ref location))
{
// Start sliding
_isSliding = true;
StartMouseCapture();
SlidingStart?.Invoke();
return true;
}
else
{
// Click change
switch (Direction)
{
case SliderDirection.HorizontalRight or SliderDirection.VerticalDown:
Value += (mousePosition < _thumbCenter ? -1 : 1) * 10;
break;
case SliderDirection.HorizontalLeft or SliderDirection.VerticalUp:
Value -= (mousePosition < _thumbCenter ? -1 : 1) * 10;
break;
default: break;
}
}
// Control slider via navigation actions
if (IsNavFocused && _isSliding)
{
var isNavUp = direction == NavDirection.Right || direction == NavDirection.Up;
var isDirUp = _direction == SliderDirection.HorizontalRight || _direction == SliderDirection.VerticalUp;
Value += (isNavUp == isDirUp ? 1 : -1) * _step;
return this;
}
return base.OnMouseDown(location, button);
return base.OnNavigate(direction, location, caller, visited);
}
/// <inheritdoc />
public override bool OnKeyDown(KeyboardKeys key)
{
if (key == KeyboardKeys.Escape)
{
Defocus();
return true;
}
return base.OnKeyDown(key);
}
private void OnClick(Float2 location)
{
Focus();
float mousePosition = Direction is SliderDirection.HorizontalRight or SliderDirection.HorizontalLeft ? location.X : location.Y;
if (_thumbRect.Contains(ref location))
{
StartSliding();
}
else
{
// Click change
switch (Direction)
{
case SliderDirection.HorizontalRight or SliderDirection.VerticalDown:
Value += (mousePosition < _thumbCenter ? -1 : 1) * _step;
break;
case SliderDirection.HorizontalLeft or SliderDirection.VerticalUp:
Value -= (mousePosition < _thumbCenter ? -1 : 1) * _step;
break;
default: break;
}
}
}
/// <inheritdoc />
public override bool OnMouseDown(Float2 location, MouseButton button)
{
if (base.OnMouseDown(location, button))
return true;
if (button == MouseButton.Left)
{
OnClick(location);
return true;
}
return false;
}
/// <inheritdoc />
public override bool OnTouchDown(Float2 location, int pointerId)
{
if (base.OnTouchDown(location, pointerId))
return true;
OnClick(location);
return true;
}
/// <inheritdoc />
@@ -474,6 +529,18 @@ public class Slider : ContainerControl
}
}
/// <inheritdoc />
public override void OnKeyUp(KeyboardKeys key)
{
if (key == KeyboardKeys.Escape && _isSliding)
{
EndSliding();
return;
}
base.OnKeyUp(key);
}
/// <inheritdoc />
public override bool OnMouseUp(Float2 location, MouseButton button)
{
@@ -486,6 +553,45 @@ public class Slider : ContainerControl
return base.OnMouseUp(location, button);
}
/// <inheritdoc />
public override bool OnTouchUp(Float2 location, int pointerId)
{
if (base.OnTouchUp(location, pointerId))
return true;
if (_isSliding)
{
EndSliding();
}
return true;
}
/// <inheritdoc />
public override void OnTouchMove(Float2 location, int pointerId)
{
base.OnTouchMove(location, pointerId);
if (_isSliding)
{
OnMouseMove(location);
}
}
/// <inheritdoc />
public override void OnSubmit()
{
base.OnSubmit();
if (_isSliding)
{
EndSliding();
}
else
{
StartSliding();
}
}
/// <inheritdoc />
public override void OnEndMouseCapture()
{
+5 -5
View File
@@ -66,7 +66,7 @@ namespace FlaxEngine.GUI
}
/// <summary>
/// The vertical alignment of the text.
/// The horizontal alignment of the text.
/// </summary>
[EditorDisplay("Text Style"), EditorOrder(2024), Tooltip("The horizontal alignment of the text.")]
public TextAlignment HorizontalAlignment
@@ -268,7 +268,7 @@ namespace FlaxEngine.GUI
if (selectedLinesCount == 1)
{
// Selected is part of single line
Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, rightEdge.X - leftEdge.X, fontHeight);
Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, rightEdge.X - leftEdge.X, textHeight);
Render2D.FillRectangle(r1, selectionColor);
}
else
@@ -276,17 +276,17 @@ namespace FlaxEngine.GUI
float leftMargin = _layout.Bounds.Location.X;
// Selected is more than one line
Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, 1000000000, fontHeight);
Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, 1000000000, textHeight);
Render2D.FillRectangle(r1, selectionColor);
//
for (int i = 3; i <= selectedLinesCount; i++)
{
leftEdge.Y += textHeight;
Rectangle r = new Rectangle(leftMargin, leftEdge.Y, 1000000000, fontHeight);
Rectangle r = new Rectangle(leftMargin, leftEdge.Y, 1000000000, textHeight);
Render2D.FillRectangle(r, selectionColor);
}
//
Rectangle r2 = new Rectangle(leftMargin, rightEdge.Y, rightEdge.X - leftMargin, fontHeight);
Rectangle r2 = new Rectangle(leftMargin, rightEdge.Y, rightEdge.X - leftMargin, textHeight);
Render2D.FillRectangle(r2, selectionColor);
}
}
+1 -1
View File
@@ -563,7 +563,7 @@ namespace FlaxEngine.GUI
caretPos.X - (caretWidth * 0.5f),
caretPos.Y,
caretWidth,
height * DpiScale);
height);
}
}
+4 -1
View File
@@ -162,7 +162,10 @@ namespace FlaxEngine.GUI
if (_splitterClicked)
{
SplitterValue = _orientation == Orientation.Horizontal ? location.X / Width : location.Y / Height;
if (_orientation == Orientation.Horizontal && Width > 0)
SplitterValue = location.X / Width;
else if (_orientation == Orientation.Vertical && Height > 0)
SplitterValue = location.Y / Height;
Cursor = _orientation == Orientation.Horizontal ? CursorType.SizeWE : CursorType.SizeNS;
_cursorChanged = true;
}
@@ -295,6 +295,7 @@ ${PBXResourcesGroup}
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = FlaxGame/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "${ProjectName}";
INFOPLIST_KEY_CADisableMinimumFrameDurationOnPhone = YES;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
@@ -336,6 +337,7 @@ ${PBXResourcesGroup}
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = FlaxGame/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "${ProjectName}";
INFOPLIST_KEY_CADisableMinimumFrameDurationOnPhone = YES;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
@@ -204,6 +204,8 @@ namespace Flax.Build
projectFormats.Add(ProjectFormat.VisualStudio2015);
if (Configuration.ProjectFormatVSCode)
projectFormats.Add(ProjectFormat.VisualStudioCode);
if (Configuration.ProjectFormatCLion)
projectFormats.Add(ProjectFormat.CMake);
if (Configuration.ProjectFormatRider)
projectFormats.Add(ProjectFormat.VisualStudio2026);
if (!string.IsNullOrEmpty(Configuration.ProjectFormatCustom))
+6
View File
@@ -213,6 +213,12 @@ namespace Flax.Build
[CommandLine("vscode", "Generates Visual Studio Code project format files. Valid only with -genproject option.")]
public static bool ProjectFormatVSCode = false;
/// <summary>
/// Generates CMake facade project files for CLion. Valid only with -genproject option.
/// </summary>
[CommandLine("clion", "Generates CMake facade project files for CLion. Valid only with -genproject option.")]
public static bool ProjectFormatCLion = false;
/// <summary>
/// Generates Visual Studio 2022 project format files for Rider. Valid only with -genproject option.
/// </summary>
@@ -22,6 +22,7 @@ namespace Flax.Build.Platforms
{
TargetPlatform.Windows,
TargetPlatform.Linux,
TargetPlatform.Mac,
};
/// <summary>
@@ -22,6 +22,7 @@ namespace Flax.Build.Platforms
{
TargetPlatform.Windows,
TargetPlatform.Linux,
TargetPlatform.Mac,
};
/// <summary>
@@ -106,6 +107,9 @@ namespace Flax.Build.Platforms
case TargetPlatform.Linux:
hostName = "linux-x86_64";
break;
case TargetPlatform.Mac:
hostName = "darwin-x86_64";
break;
default: throw new InvalidPlatformException(Platform.BuildPlatform.Target);
}
return hostName;
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Flax.Build.Graph;
using Flax.Build.NativeCpp;
@@ -37,10 +38,20 @@ namespace Flax.Build.Platforms
: base(platform, architecture, toolchainRoot, null, string.Empty)
{
var toolchain = ToolsetRoot.Replace('\\', '/');
SystemIncludePaths.Add(Path.Combine(toolchain, "sources/usr/include/c++/v1").Replace('\\', '/'));
var cxxIncludePath = Path.Combine(toolchain, "sysroot/usr/include/c++/v1");
if (!Directory.Exists(cxxIncludePath))
cxxIncludePath = Path.Combine(toolchain, "sources/usr/include/c++/v1");
SystemIncludePaths.Add(cxxIncludePath.Replace('\\', '/'));
SystemIncludePaths.Add(Path.Combine(toolchain, "sysroot/usr/include").Replace('\\', '/'));
SystemIncludePaths.Add(Path.Combine(toolchain, "sysroot/usr/local/include").Replace('\\', '/'));
SystemIncludePaths.Add(Path.Combine(toolchain, "lib64/clang/9.0.8/include").Replace('\\', '/'));
var clangIncludeRoot = Path.Combine(toolchain, "lib/clang");
if (!Directory.Exists(clangIncludeRoot))
clangIncludeRoot = Path.Combine(toolchain, "lib64/clang");
var clangIncludePath = Directory.Exists(clangIncludeRoot)
? Directory.GetDirectories(clangIncludeRoot).OrderBy(x => x).LastOrDefault()
: null;
if (clangIncludePath != null)
SystemIncludePaths.Add(Path.Combine(clangIncludePath, "include").Replace('\\', '/'));
}
/// <summary>
@@ -0,0 +1,624 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Flax.Build.NativeCpp;
namespace Flax.Build.Projects.CMake
{
/// <summary>
/// Project generator for CMake-based IDE facade.
/// </summary>
public class CMakeProjectGenerator : ProjectGenerator
{
/// <inheritdoc />
public override string ProjectFileExtension => "cmake";
/// <inheritdoc />
public override string SolutionFileExtension => "cmake";
/// <inheritdoc />
public override TargetType? Type => null;
/// <inheritdoc />
public override void GenerateProject(Project project, string solutionPath, bool isMainProject)
{
// Not used, solution contains all CMake project definitions.
}
/// <inheritdoc />
public override void GenerateSolution(Solution solution)
{
var projectFolder = GetProjectFolder(solution);
if (!Directory.Exists(projectFolder))
Directory.CreateDirectory(projectFolder);
var generateCLionFiles = Configuration.ProjectFormatCLion;
var contents = new StringBuilder();
var buildPresets = new List<CMakeBuildPreset>();
var defaultBuildTarget = string.Empty;
var targetNames = new HashSet<string>();
var buildToolPath = Path.ChangeExtension(typeof(Builder).Assembly.Location, null) + Utilities.GetPlatformExecutableExt();
contents.AppendLine("cmake_minimum_required(VERSION 3.20)");
contents.AppendLine();
contents.AppendLine("# Generated by Flax.Build. Do not edit manually.");
contents.AppendLine("# CMake is used as an IDE facade. Actual builds are delegated to Flax.Build.");
contents.AppendLine($"project({EscapeIdentifier(solution.Name)} LANGUAGES C CXX)");
contents.AppendLine();
contents.AppendLine("set(CMAKE_EXPORT_COMPILE_COMMANDS ON)");
contents.AppendLine($"set(FLAX_WORKSPACE {QuotePath(solution.WorkspaceRootPath, solution.WorkspaceRootPath)})");
contents.AppendLine($"set(FLAX_BUILD_TOOL {QuotePath(buildToolPath, solution.WorkspaceRootPath)})");
if (DotNetSdk.Instance.IsValid)
contents.AppendLine($"set(FLAX_DOTNET_ROOT {QuotePath(DotNetSdk.Instance.RootPath, solution.WorkspaceRootPath)})");
contents.AppendLine();
foreach (var project in solution.Projects.Where(x => x.Type == TargetType.NativeCpp))
{
var visibleFiles = GetProjectFiles(project, generateCLionFiles);
var hasCodeModelConfiguration = TryGetCodeModelConfiguration(project, out var codeModelConfiguration);
// Use Flax.Build-selected compile units for the code model. The wider project scan is only for visible files.
var compileFiles = hasCodeModelConfiguration
? GetCodeModelCompileFiles(codeModelConfiguration, generateCLionFiles)
: visibleFiles.Where(x => IsCompileSourceFile(x, generateCLionFiles)).ToArray();
var files = new HashSet<string>(visibleFiles);
files.AddRange(compileFiles);
var allFiles = files.OrderBy(x => x).ToArray();
var compileFileSet = new HashSet<string>(compileFiles);
var nonCompileFiles = allFiles.Where(x => !compileFileSet.Contains(x)).ToArray();
var includePaths = hasCodeModelConfiguration ? GetIncludePaths(project, codeModelConfiguration) : Array.Empty<string>();
var definitions = hasCodeModelConfiguration ? GetPreprocessorDefinitions(project, codeModelConfiguration) : project.Defines.OrderBy(x => x).ToArray();
var cppStandard = hasCodeModelConfiguration ? GetCppStandard(codeModelConfiguration) : 14;
var ideTargetName = GetUniqueTargetName(targetNames, "IDE_" + project.Name);
contents.AppendLine($"# {project.Name}");
if (compileFiles.Length != 0)
{
contents.AppendLine($"add_library({ideTargetName} STATIC EXCLUDE_FROM_ALL)");
contents.AppendLine($"set_target_properties({ideTargetName} PROPERTIES EXCLUDE_FROM_ALL TRUE)");
AppendTargetSources(contents, ideTargetName, allFiles, solution.WorkspaceRootPath);
if (nonCompileFiles.Length != 0)
{
contents.AppendLine("set_source_files_properties(");
foreach (var file in nonCompileFiles)
contents.AppendLine($" {QuotePath(file, solution.WorkspaceRootPath)}");
contents.AppendLine(" PROPERTIES HEADER_FILE_ONLY TRUE");
contents.AppendLine(")");
}
contents.AppendLine($"set_target_properties({ideTargetName} PROPERTIES CXX_STANDARD {cppStandard} CXX_STANDARD_REQUIRED YES)");
AppendTargetPaths(contents, ideTargetName, "target_include_directories", "PRIVATE", includePaths, solution.WorkspaceRootPath);
AppendTargetValues(contents, ideTargetName, "target_compile_definitions", "PRIVATE", definitions);
}
else if (allFiles.Length != 0)
{
contents.AppendLine($"add_custom_target({ideTargetName} SOURCES");
foreach (var file in allFiles)
contents.AppendLine($" {QuotePath(file, solution.WorkspaceRootPath)}");
contents.AppendLine(")");
}
foreach (var configuration in project.Configurations)
{
if (generateCLionFiles && !IsCLionDevelopmentPlatform(configuration.Platform))
continue;
var buildTargetName = GetUniqueTargetName(targetNames, "Build_" + project.Name + "_" + configuration.Target.Name + "_" + configuration.PlatformName + "_" + configuration.ConfigurationName + "_" + configuration.ArchitectureName);
AppendBuildTarget(contents, buildTargetName, "-build", configuration);
var presetSuffix = project.Name + "-" + configuration.Target.Name + "-" + configuration.PlatformName + "-" + configuration.ConfigurationName + "-" + configuration.ArchitectureName;
if (IsHostConfiguration(configuration))
AddBuildPreset(buildPresets, "build-" + presetSuffix, "Build " + presetSuffix, buildTargetName);
if (defaultBuildTarget.Length == 0 && project == solution.MainProject && configuration.Platform == Platform.BuildPlatform.Target && configuration.Architecture == Platform.BuildTargetArchitecture && configuration.Configuration == TargetConfiguration.Development)
defaultBuildTarget = buildTargetName;
if (generateCLionFiles)
{
var runProgram = GetRunProgram(project, configuration, out var runArguments);
if (IsHostConfiguration(configuration) && !string.IsNullOrEmpty(runProgram))
{
var runTargetName = GetUniqueTargetName(targetNames, "App_" + project.Name + "_" + configuration.Target.Name + "_" + configuration.PlatformName + "_" + configuration.ConfigurationName + "_" + configuration.ArchitectureName);
AppendRunTarget(contents, runTargetName, runProgram, runArguments);
contents.AppendLine($"add_dependencies({runTargetName} {buildTargetName})");
contents.AppendLine();
}
}
}
contents.AppendLine();
}
if (!string.IsNullOrEmpty(defaultBuildTarget))
{
contents.AppendLine($"add_custom_target({EscapeIdentifier(solution.Name)}_Build ALL DEPENDS {defaultBuildTarget})");
contents.AppendLine();
}
Utilities.WriteFileIfChanged(Path.Combine(projectFolder, "CMakeLists.txt"), contents.ToString());
GeneratePresets(solution, projectFolder, buildPresets);
}
/// <summary>
/// Gets the directory that contains the generated CMake facade project.
/// </summary>
public static string GetProjectFolder(Solution solution)
{
return Path.Combine(solution.WorkspaceRootPath, "Cache", "Projects", "CMake", solution.Name);
}
private struct CMakeBuildPreset
{
public string Name;
public string DisplayName;
public string Target;
}
private static void AddBuildPreset(List<CMakeBuildPreset> presets, string name, string displayName, string target)
{
presets.Add(new CMakeBuildPreset
{
Name = EscapePresetName(name),
DisplayName = displayName,
Target = target,
});
}
private static void GeneratePresets(Solution solution, string projectFolder, List<CMakeBuildPreset> buildPresets)
{
var contents = new StringBuilder();
contents.AppendLine("{");
contents.AppendLine(" \"version\": 3,");
contents.AppendLine(" \"cmakeMinimumRequired\": {");
contents.AppendLine(" \"major\": 3,");
contents.AppendLine(" \"minor\": 20,");
contents.AppendLine(" \"patch\": 0");
contents.AppendLine(" },");
contents.AppendLine(" \"configurePresets\": [");
contents.AppendLine(" {");
contents.AppendLine(" \"name\": \"flax\",");
contents.AppendLine(" \"displayName\": \"Flax\",");
contents.AppendLine(" \"description\": \"Configure Flax CMake facade for IDE integration.\",");
contents.AppendLine(" \"generator\": \"Ninja\",");
contents.AppendLine(" \"binaryDir\": \"${sourceDir}/cmake-build-flax\",");
contents.AppendLine(" \"cacheVariables\": {");
contents.AppendLine(" \"CMAKE_BUILD_TYPE\": \"Debug\",");
contents.AppendLine(" \"CMAKE_EXPORT_COMPILE_COMMANDS\": \"ON\"");
contents.AppendLine(" }");
contents.AppendLine(" }");
contents.AppendLine(" ],");
contents.AppendLine(" \"buildPresets\": [");
for (var i = 0; i < buildPresets.Count; i++)
{
var preset = buildPresets[i];
contents.AppendLine(" {");
contents.AppendLine($" \"name\": {JsonString(preset.Name)},");
contents.AppendLine($" \"displayName\": {JsonString(preset.DisplayName)},");
contents.AppendLine(" \"configurePreset\": \"flax\",");
contents.AppendLine($" \"targets\": [{JsonString(preset.Target)}]");
contents.Append(" }");
if (i != buildPresets.Count - 1)
contents.Append(',');
contents.AppendLine();
}
contents.AppendLine(" ]");
contents.AppendLine("}");
Utilities.WriteFileIfChanged(Path.Combine(projectFolder, "CMakePresets.json"), contents.ToString());
}
private static void AppendBuildTarget(StringBuilder contents, string targetName, string action, Project.ConfigurationData configuration)
{
contents.AppendLine($"add_custom_target({targetName}");
contents.Append(" COMMAND ");
if (DotNetSdk.Instance.IsValid)
contents.Append("${CMAKE_COMMAND} -E env \"DOTNET_ROOT=${FLAX_DOTNET_ROOT}\" ");
contents.AppendLine($"\"${{FLAX_BUILD_TOOL}}\" {Quote(action)} \"-log\" \"-mutex\"");
contents.AppendLine(" \"-workspace=${FLAX_WORKSPACE}\"");
contents.AppendLine($" \"-arch={configuration.ArchitectureName}\"");
contents.AppendLine($" \"-configuration={configuration.ConfigurationName}\"");
contents.AppendLine($" \"-platform={configuration.PlatformName}\"");
contents.AppendLine($" \"-buildTargets={configuration.Target.Name}\"");
if (!string.IsNullOrEmpty(Configuration.Compiler))
contents.AppendLine($" \"-compiler={EscapeString(Configuration.Compiler)}\"");
if (!string.IsNullOrEmpty(Configuration.Dotnet))
contents.AppendLine($" \"-dotnet={EscapeString(Configuration.Dotnet)}\"");
if (Configuration.Sanitizers != Sanitizer.None)
contents.AppendLine($" \"-sanitizers={Configuration.Sanitizers}\"");
contents.AppendLine(" WORKING_DIRECTORY \"${FLAX_WORKSPACE}\"");
contents.AppendLine(" USES_TERMINAL");
contents.AppendLine(" VERBATIM");
contents.AppendLine(")");
contents.AppendLine();
}
private static void AppendRunTarget(StringBuilder contents, string targetName, string program, string[] arguments)
{
var sourcePath = $"${{CMAKE_CURRENT_BINARY_DIR}}/{targetName}.cpp";
contents.AppendLine($"file(WRITE \"{sourcePath}\" [=[");
contents.AppendLine("#include <cstdio>");
contents.AppendLine("#include <vector>");
contents.AppendLine("#ifdef _WIN32");
contents.AppendLine("#include <process.h>");
contents.AppendLine("#else");
contents.AppendLine("#include <unistd.h>");
contents.AppendLine("#endif");
contents.AppendLine("int main(int argc, char** argv) {");
contents.AppendLine($" const char* program = \"{CppString(program)}\";");
contents.AppendLine(" std::vector<const char*> args;");
contents.AppendLine(" args.reserve(argc + 1);");
contents.AppendLine(" args.push_back(program);");
foreach (var arg in arguments)
contents.AppendLine($" args.push_back(\"{CppString(arg)}\");");
contents.AppendLine(" for (int i = 1; i < argc; i++)");
contents.AppendLine(" args.push_back(argv[i]);");
contents.AppendLine(" args.push_back(nullptr);");
contents.AppendLine("#ifdef _WIN32");
contents.AppendLine(" _execv(args[0], args.data());");
contents.AppendLine("#else");
contents.AppendLine(" execv(args[0], const_cast<char* const*>(args.data()));");
contents.AppendLine("#endif");
contents.AppendLine(" perror(args[0]);");
contents.AppendLine(" return 1;");
contents.AppendLine("}");
contents.AppendLine("]=])");
contents.AppendLine($"add_executable({targetName} EXCLUDE_FROM_ALL \"{sourcePath}\")");
contents.AppendLine($"set_target_properties({targetName} PROPERTIES");
contents.AppendLine(" RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_CURRENT_BINARY_DIR}/FlaxLaunchers\"");
contents.AppendLine(")");
}
private static void AppendTargetSources(StringBuilder contents, string targetName, string[] files, string workspaceRoot)
{
contents.AppendLine($"target_sources({targetName} PRIVATE");
foreach (var file in files)
contents.AppendLine($" {QuotePath(file, workspaceRoot)}");
contents.AppendLine(")");
}
private static void AppendTargetPaths(StringBuilder contents, string targetName, string command, string visibility, string[] values, string workspaceRoot)
{
if (values.Length == 0)
return;
contents.AppendLine($"{command}({targetName} {visibility}");
foreach (var value in values)
contents.AppendLine($" {QuotePath(value, workspaceRoot)}");
contents.AppendLine(")");
}
private static void AppendTargetValues(StringBuilder contents, string targetName, string command, string visibility, string[] values)
{
if (values.Length == 0)
return;
contents.AppendLine($"{command}({targetName} {visibility}");
foreach (var value in values)
contents.AppendLine($" {Quote(value)}");
contents.AppendLine(")");
}
private static string[] GetProjectFiles(Project project, bool generateCLionFiles)
{
var files = new HashSet<string>();
if (project.SourceFiles != null)
files.AddRange(project.SourceFiles);
if (project.GeneratedSourceFiles != null)
files.AddRange(project.GeneratedSourceFiles);
if (project.SourceDirectories != null)
{
foreach (var folder in project.SourceDirectories)
{
if (Directory.Exists(folder))
files.AddRange(Directory.GetFiles(folder, "*", SearchOption.AllDirectories));
}
}
files.RemoveWhere(x =>
!IsProjectFile(x) ||
Path.GetFileName(x).Equals(".DS_Store", StringComparison.OrdinalIgnoreCase) ||
(generateCLionFiles && !IsCLionDevelopmentSourceFile(x)));
return files.OrderBy(x => x).ToArray();
}
private static string[] GetCodeModelCompileFiles(Project.ConfigurationData configuration, bool generateCLionFiles)
{
var files = new HashSet<string>();
var definitions = new HashSet<string>(configuration.TargetBuildOptions.CompileEnv.PreprocessorDefinitions);
AddCompileFiles(files, configuration.TargetBuildOptions.SourceFiles, generateCLionFiles, definitions);
if (configuration.Modules != null)
{
foreach (var module in configuration.Modules)
{
if (module.Key.BuildNativeCode)
AddCompileFiles(files, module.Value.SourceFiles, generateCLionFiles, definitions);
}
}
return files.OrderBy(x => x).ToArray();
}
private static void AddCompileFiles(HashSet<string> files, IEnumerable<string> sourceFiles, bool generateCLionFiles, HashSet<string> definitions = null)
{
if (sourceFiles == null)
return;
foreach (var file in sourceFiles)
{
if (IsCompileSourceFile(file, generateCLionFiles, definitions))
files.Add(file);
}
}
private static bool TryGetCodeModelConfiguration(Project project, out Project.ConfigurationData configuration)
{
foreach (var e in project.Configurations)
{
if (e.TargetBuildOptions != null &&
e.Platform == Platform.BuildPlatform.Target &&
e.Architecture == Platform.BuildTargetArchitecture &&
e.Configuration == TargetConfiguration.Development)
{
configuration = e;
return true;
}
}
foreach (var e in project.Configurations)
{
if (e.TargetBuildOptions != null)
{
configuration = e;
return true;
}
}
configuration = default;
return false;
}
private static bool IsHostConfiguration(Project.ConfigurationData configuration)
{
return configuration.Platform == Platform.BuildPlatform.Target &&
configuration.Architecture == Platform.BuildTargetArchitecture;
}
private static bool IsCLionDevelopmentPlatform(TargetPlatform platform)
{
switch (platform)
{
case TargetPlatform.Windows:
case TargetPlatform.Linux:
case TargetPlatform.Mac:
return true;
default:
return false;
}
}
private static string[] GetIncludePaths(Project project, Project.ConfigurationData configuration)
{
var result = new HashSet<string>();
if (project.SearchPaths != null)
result.AddRange(project.SearchPaths);
result.AddRange(configuration.TargetBuildOptions.CompileEnv.IncludePaths);
return result.OrderBy(x => x).ToArray();
}
private static string[] GetPreprocessorDefinitions(Project project, Project.ConfigurationData configuration)
{
var result = new HashSet<string>();
result.AddRange(project.Defines);
result.AddRange(configuration.TargetBuildOptions.CompileEnv.PreprocessorDefinitions);
return result.OrderBy(x => x).ToArray();
}
private static int GetCppStandard(Project.ConfigurationData configuration)
{
var result = 14;
switch (configuration.TargetBuildOptions.CompileEnv.CppVersion)
{
case CppVersion.Cpp17:
result = Math.Max(result, 17);
break;
case CppVersion.Cpp20:
case CppVersion.Latest:
result = Math.Max(result, 20);
break;
}
return result;
}
private static string GetRunProgram(Project project, Project.ConfigurationData configuration, out string[] arguments)
{
var target = configuration.Target;
var outputType = project.OutputType ?? target.OutputType;
if (ShouldRunEditorForProject(configuration, outputType))
{
var program = Path.Combine(Globals.EngineRoot, Platform.GetEditorBinaryDirectory(), configuration.ConfigurationName, "FlaxEditor" + Utilities.GetPlatformExecutableExt());
var args = new List<string>();
if (configuration.Platform == TargetPlatform.Linux || configuration.Platform == TargetPlatform.Mac)
args.Add("-std");
args.Add("-project");
args.Add(project.WorkspaceRootPath);
args.Add("-skipCompile");
arguments = args.ToArray();
return program;
}
arguments = configuration.Platform == TargetPlatform.Linux || configuration.Platform == TargetPlatform.Mac ? new[] { "--std" } : Array.Empty<string>();
return target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable);
}
private static bool ShouldRunEditorForProject(Project.ConfigurationData configuration, TargetOutputType outputType)
{
return outputType != TargetOutputType.Executable && configuration.Target.IsEditor;
}
private static bool IsCppCompileFile(string path)
{
var extension = Path.GetExtension(path);
return extension.Equals(".c", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".cc", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".cpp", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".cxx", StringComparison.OrdinalIgnoreCase);
}
private static bool IsCompileSourceFile(string path, bool generateCLionFiles, HashSet<string> definitions = null)
{
return IsCppCompileFile(path) &&
(!generateCLionFiles || (IsCLionDevelopmentSourceFile(path) && IsCLionSourceGuardActive(path, definitions)));
}
private static bool IsCLionDevelopmentSourceFile(string path)
{
path = Utilities.NormalizePath(path);
return !ContainsPathSegment(path, "Android") &&
!ContainsPathSegment(path, "AndroidJNI") &&
!ContainsPathSegment(path, "GDK") &&
!ContainsPathSegment(path, "iOS") &&
!ContainsPathSegment(path, "PS4") &&
!ContainsPathSegment(path, "PS5") &&
!ContainsPathSegment(path, "Switch") &&
!ContainsPathSegment(path, "UWP") &&
!ContainsPathSegment(path, "Web") &&
!ContainsPathSegment(path, "XboxOne") &&
!ContainsPathSegment(path, "XboxScarlett") &&
!Path.GetFileName(path).Contains(".Web.", StringComparison.OrdinalIgnoreCase);
}
private static bool ContainsPathSegment(string path, string segment)
{
return path.IndexOf("/" + segment + "/", StringComparison.OrdinalIgnoreCase) != -1;
}
private static bool IsCLionSourceGuardActive(string path, HashSet<string> definitions)
{
if (definitions == null)
return true;
path = Utilities.NormalizePath(path);
if (ContainsSourcePath(path, "Source/Editor/Cooker/Platform/Windows"))
return HasDefinition(definitions, "PLATFORM_TOOLS_WINDOWS");
if (ContainsSourcePath(path, "Source/Editor/Cooker/Platform/Linux"))
return HasDefinition(definitions, "PLATFORM_TOOLS_LINUX");
if (ContainsSourcePath(path, "Source/Editor/Cooker/Platform/Mac"))
return HasDefinition(definitions, "PLATFORM_TOOLS_MAC");
if (ContainsSourcePath(path, "Source/Engine/Engine/Windows"))
return HasDefinition(definitions, "PLATFORM_WINDOWS") && !HasDefinition(definitions, "USE_EDITOR");
if (ContainsSourcePath(path, "Source/Engine/Engine/Linux"))
return HasDefinition(definitions, "PLATFORM_LINUX") && !HasDefinition(definitions, "USE_EDITOR");
if (ContainsSourcePath(path, "Source/Engine/Engine/Mac"))
return HasDefinition(definitions, "PLATFORM_MAC") && !HasDefinition(definitions, "USE_EDITOR");
if (ContainsSourcePath(path, "Source/Engine/GraphicsDevice/Vulkan/Win32"))
return HasDefinition(definitions, "GRAPHICS_API_VULKAN") && (HasDefinition(definitions, "PLATFORM_WIN32") || HasDefinition(definitions, "PLATFORM_WINDOWS"));
if (ContainsSourcePath(path, "Source/Engine/GraphicsDevice/Vulkan/Linux"))
return HasDefinition(definitions, "GRAPHICS_API_VULKAN") && HasDefinition(definitions, "PLATFORM_LINUX");
if (ContainsSourcePath(path, "Source/Engine/GraphicsDevice/Vulkan/Mac"))
return HasDefinition(definitions, "GRAPHICS_API_VULKAN") && HasDefinition(definitions, "PLATFORM_MAC");
if (ContainsSourcePath(path, "Source/Engine/Platform/Win32"))
return HasDefinition(definitions, "PLATFORM_WIN32") || HasDefinition(definitions, "PLATFORM_WINDOWS");
if (ContainsSourcePath(path, "Source/Engine/Platform/Windows"))
return HasDefinition(definitions, "PLATFORM_WINDOWS");
if (ContainsSourcePath(path, "Source/Engine/Platform/Linux"))
return HasDefinition(definitions, "PLATFORM_LINUX");
if (ContainsSourcePath(path, "Source/Engine/Platform/Mac"))
return HasDefinition(definitions, "PLATFORM_MAC");
if (ContainsSourcePath(path, "Source/Engine/Platform/Apple"))
return HasDefinition(definitions, "PLATFORM_MAC") || HasDefinition(definitions, "PLATFORM_IOS");
if (ContainsSourcePath(path, "Source/Engine/Platform/Unix"))
return HasDefinition(definitions, "PLATFORM_UNIX");
return true;
}
private static bool ContainsSourcePath(string path, string sourcePath)
{
return path.IndexOf("/" + sourcePath + "/", StringComparison.OrdinalIgnoreCase) != -1;
}
private static bool HasDefinition(HashSet<string> definitions, string name)
{
return definitions.Contains(name) ||
definitions.Any(x => x.StartsWith(name + "=", StringComparison.Ordinal));
}
private static bool IsProjectFile(string path)
{
var extension = Path.GetExtension(path);
return IsCppCompileFile(path) ||
extension.Equals(".h", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".hh", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".hpp", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".hxx", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".inl", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".rc", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".manifest", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".natvis", StringComparison.OrdinalIgnoreCase);
}
private static string GetUniqueTargetName(HashSet<string> names, string name)
{
var result = EscapeIdentifier(name);
var baseName = result;
var index = 2;
while (!names.Add(result))
result = baseName + "_" + index++;
return result;
}
private static string EscapeIdentifier(string value)
{
if (string.IsNullOrEmpty(value))
return "_";
var result = new StringBuilder(value.Length);
foreach (var c in value)
result.Append(char.IsLetterOrDigit(c) || c == '_' ? c : '_');
if (char.IsDigit(result[0]))
result.Insert(0, '_');
return result.ToString();
}
private static string EscapePresetName(string value)
{
var result = new StringBuilder(value.Length);
foreach (var c in value)
result.Append(char.IsLetterOrDigit(c) || c == '-' || c == '_' || c == '.' ? char.ToLowerInvariant(c) : '-');
return result.ToString();
}
private static string QuotePath(string value, string workspaceRoot, bool includeQuotes = true)
{
value = Utilities.NormalizePath(value);
workspaceRoot = Utilities.NormalizePath(workspaceRoot).TrimEnd('/');
if (value.StartsWith(workspaceRoot + "/", StringComparison.Ordinal))
value = "${FLAX_WORKSPACE}/" + value.Substring(workspaceRoot.Length + 1);
return Quote(value, includeQuotes);
}
private static string Quote(string value, bool includeQuotes = true)
{
value = EscapeString(value);
return includeQuotes ? "\"" + value + "\"" : value;
}
private static string EscapeString(string value)
{
return value.Replace("\\", "\\\\").Replace(";", "\\;").Replace("\"", "\\\"");
}
private static string JsonString(string value)
{
return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
}
private static string CppString(string value)
{
return value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
}
}
}
@@ -47,6 +47,11 @@ namespace Flax.Build.Projects
/// </summary>
VisualStudioCode,
/// <summary>
/// CMake.
/// </summary>
CMake,
/// <summary>
/// XCode.
/// </summary>
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using Flax.Build.Projects.CMake;
using Flax.Build.Projects.VisualStudio;
using Flax.Build.Projects.VisualStudioCode;
@@ -150,6 +151,7 @@ namespace Flax.Build.Projects
case ProjectFormat.VisualStudioCode: return type == TargetType.DotNet
? (ProjectGenerator)new CSProjectGenerator(VisualStudioVersion.VisualStudio2015)
: (ProjectGenerator)new VisualStudioCodeProjectGenerator();
case ProjectFormat.CMake: return new CMakeProjectGenerator();
case ProjectFormat.XCode: return new XCodeProjectGenerator();
case ProjectFormat.Custom:
if (CustomProjectTypes.TryGetValue(Configuration.ProjectFormatCustom, out var factory))