723 lines
25 KiB
C++
723 lines
25 KiB
C++
#pragma once
|
|
|
|
#include "Application.h"
|
|
#include "BuiltInIcons.h"
|
|
#include "Core.h"
|
|
#include "Core/IEditorContext.h"
|
|
#include "Core/IProjectManager.h"
|
|
#include "Core/ISceneManager.h"
|
|
#include "SearchText.h"
|
|
#include "StyleTokens.h"
|
|
#include "Utils/ProjectFileUtils.h"
|
|
#include "Widgets.h"
|
|
|
|
#include <XCEngine/Components/GameObject.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cctype>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <functional>
|
|
#include <initializer_list>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace XCEngine {
|
|
namespace Editor {
|
|
namespace UI {
|
|
|
|
enum class ReferencePickerTab {
|
|
Assets = 0,
|
|
Scene = 1
|
|
};
|
|
|
|
struct ReferencePickerOptions {
|
|
const char* popupTitle = "Select Object";
|
|
const char* emptyHint = "None";
|
|
const char* searchHint = "Search";
|
|
const char* noAssetsText = "No compatible assets.";
|
|
const char* noSceneText = "No compatible scene objects.";
|
|
const char* assetsTabLabel = "Assets";
|
|
const char* sceneTabLabel = "Scene";
|
|
bool showAssetsTab = true;
|
|
bool showSceneTab = true;
|
|
std::initializer_list<const char*> supportedAssetExtensions;
|
|
std::function<bool(const ::XCEngine::Components::GameObject&)> sceneFilter;
|
|
};
|
|
|
|
struct ReferencePickerInteraction {
|
|
std::string assignedAssetPath;
|
|
::XCEngine::Components::GameObject::ID assignedSceneObjectId =
|
|
::XCEngine::Components::GameObject::INVALID_ID;
|
|
bool clearRequested = false;
|
|
};
|
|
|
|
namespace Detail {
|
|
|
|
struct ReferencePickerState {
|
|
ReferencePickerTab activeTab = ReferencePickerTab::Assets;
|
|
std::array<char, 128> searchBuffer{};
|
|
};
|
|
|
|
struct ReferencePickerCandidate {
|
|
std::string label;
|
|
std::string secondaryLabel;
|
|
std::string normalizedSearchText;
|
|
AssetIconKind iconKind = AssetIconKind::File;
|
|
std::string assetPath;
|
|
::XCEngine::Components::GameObject::ID sceneObjectId =
|
|
::XCEngine::Components::GameObject::INVALID_ID;
|
|
float indent = 0.0f;
|
|
|
|
bool IsAsset() const {
|
|
return !assetPath.empty();
|
|
}
|
|
|
|
bool IsSceneObject() const {
|
|
return sceneObjectId != ::XCEngine::Components::GameObject::INVALID_ID;
|
|
}
|
|
};
|
|
|
|
inline ImVec4 ReferencePickerBackgroundColor() {
|
|
return HierarchyInspectorPanelBackgroundColor();
|
|
}
|
|
|
|
inline ImVec4 ReferencePickerSurfaceColor() {
|
|
return ProjectBrowserPaneBackgroundColor();
|
|
}
|
|
|
|
inline ImVec4 ReferencePickerBorderColor() {
|
|
return ImVec4(0.32f, 0.32f, 0.32f, 1.0f);
|
|
}
|
|
|
|
inline ImVec4 ReferencePickerTextColor() {
|
|
return ImVec4(0.84f, 0.84f, 0.84f, 1.0f);
|
|
}
|
|
|
|
inline ImVec4 ReferencePickerTextDisabledColor() {
|
|
return ImVec4(0.60f, 0.60f, 0.60f, 1.0f);
|
|
}
|
|
|
|
inline ImVec4 ReferencePickerItemColor() {
|
|
return ToolbarButtonColor(false);
|
|
}
|
|
|
|
inline ImVec4 ReferencePickerItemHoveredColor() {
|
|
return ToolbarButtonHoveredColor(false);
|
|
}
|
|
|
|
inline ImVec4 ReferencePickerItemActiveColor() {
|
|
return ToolbarButtonActiveColor();
|
|
}
|
|
|
|
inline constexpr int ReferencePickerPopupStyleVarCount() {
|
|
return 3;
|
|
}
|
|
|
|
inline constexpr int ReferencePickerPopupStyleColorCount() {
|
|
return 14;
|
|
}
|
|
|
|
inline void PushReferencePickerPopupStyle() {
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, PopupWindowPadding());
|
|
ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, PopupWindowRounding());
|
|
ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, PopupWindowBorderSize());
|
|
ImGui::PushStyleColor(ImGuiCol_PopupBg, ReferencePickerBackgroundColor());
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ReferencePickerBorderColor());
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ReferencePickerTextColor());
|
|
ImGui::PushStyleColor(ImGuiCol_TextDisabled, ReferencePickerTextDisabledColor());
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ReferencePickerSurfaceColor());
|
|
ImGui::PushStyleColor(ImGuiCol_Header, ReferencePickerItemColor());
|
|
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ReferencePickerItemHoveredColor());
|
|
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ReferencePickerItemActiveColor());
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, ReferencePickerSurfaceColor());
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ReferencePickerItemHoveredColor());
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ReferencePickerItemActiveColor());
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ReferencePickerItemColor());
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ReferencePickerItemHoveredColor());
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ReferencePickerItemActiveColor());
|
|
}
|
|
|
|
inline void PopReferencePickerPopupStyle() {
|
|
ImGui::PopStyleColor(ReferencePickerPopupStyleColorCount());
|
|
ImGui::PopStyleVar(ReferencePickerPopupStyleVarCount());
|
|
}
|
|
|
|
inline bool BeginReferencePickerPopup(const char* id, const char* title) {
|
|
PushReferencePickerPopupStyle();
|
|
const bool open = ImGui::BeginPopup(id);
|
|
if (!open) {
|
|
PopReferencePickerPopupStyle();
|
|
return false;
|
|
}
|
|
|
|
if (title && title[0] != '\0') {
|
|
ImGui::TextUnformatted(title);
|
|
ImGui::Separator();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
inline void EndReferencePickerPopup() {
|
|
ImGui::EndPopup();
|
|
PopReferencePickerPopupStyle();
|
|
}
|
|
|
|
inline ReferencePickerState& GetReferencePickerState(ImGuiID id) {
|
|
static std::unordered_map<ImGuiID, ReferencePickerState> states;
|
|
return states[id];
|
|
}
|
|
|
|
inline bool MatchesSupportedAssetExtension(
|
|
std::string_view extensionLower,
|
|
std::initializer_list<const char*> supportedExtensions) {
|
|
if (supportedExtensions.size() == 0) {
|
|
return true;
|
|
}
|
|
|
|
for (const char* supportedExtension : supportedExtensions) {
|
|
if (supportedExtension != nullptr && extensionLower == supportedExtension) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
inline AssetIconKind ResolveAssetIconKind(const AssetItemPtr& item) {
|
|
if (!item) {
|
|
return AssetIconKind::File;
|
|
}
|
|
|
|
if (item->isFolder) {
|
|
return AssetIconKind::Folder;
|
|
}
|
|
|
|
if (item->type == "Scene") {
|
|
return AssetIconKind::Scene;
|
|
}
|
|
|
|
return AssetIconKind::File;
|
|
}
|
|
|
|
inline AssetIconKind ResolveAssetIconKindFromPath(const std::string& path) {
|
|
std::string extension = std::filesystem::path(path).extension().string();
|
|
std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char ch) {
|
|
return static_cast<char>(std::tolower(ch));
|
|
});
|
|
if (extension == ".xc" || extension == ".unity" || extension == ".scene") {
|
|
return AssetIconKind::Scene;
|
|
}
|
|
return AssetIconKind::File;
|
|
}
|
|
|
|
inline std::string GetAssetDisplayName(const AssetItemPtr& item) {
|
|
if (!item) {
|
|
return {};
|
|
}
|
|
|
|
if (item->isFolder) {
|
|
return item->name;
|
|
}
|
|
|
|
return std::filesystem::path(item->name).stem().string();
|
|
}
|
|
|
|
inline std::string GetAssetDisplayNameFromPath(const std::string& path) {
|
|
if (path.empty()) {
|
|
return {};
|
|
}
|
|
|
|
return std::filesystem::path(path).stem().string();
|
|
}
|
|
|
|
inline std::string BuildSceneHierarchyPath(const ::XCEngine::Components::GameObject* gameObject) {
|
|
if (!gameObject) {
|
|
return {};
|
|
}
|
|
|
|
std::vector<std::string> names;
|
|
names.reserve(8);
|
|
const ::XCEngine::Components::GameObject* current = gameObject;
|
|
while (current) {
|
|
names.push_back(current->GetName());
|
|
current = current->GetParent();
|
|
}
|
|
|
|
std::string path;
|
|
for (auto it = names.rbegin(); it != names.rend(); ++it) {
|
|
if (!path.empty()) {
|
|
path += "/";
|
|
}
|
|
path += *it;
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
inline void CollectAssetCandidatesRecursive(
|
|
const AssetItemPtr& root,
|
|
const std::string& projectPath,
|
|
const ReferencePickerOptions& options,
|
|
std::vector<ReferencePickerCandidate>& outCandidates) {
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
for (const AssetItemPtr& child : root->children) {
|
|
if (!child) {
|
|
continue;
|
|
}
|
|
|
|
if (child->isFolder) {
|
|
CollectAssetCandidatesRecursive(child, projectPath, options, outCandidates);
|
|
continue;
|
|
}
|
|
|
|
if (!MatchesSupportedAssetExtension(child->extensionLower, options.supportedAssetExtensions)) {
|
|
continue;
|
|
}
|
|
|
|
ReferencePickerCandidate candidate;
|
|
candidate.label = GetAssetDisplayName(child);
|
|
candidate.secondaryLabel = ProjectFileUtils::MakeProjectRelativePath(projectPath, child->fullPath);
|
|
candidate.normalizedSearchText =
|
|
NormalizeSearchText(candidate.label + " " + candidate.secondaryLabel);
|
|
candidate.iconKind = ResolveAssetIconKind(child);
|
|
candidate.assetPath = std::move(candidate.secondaryLabel);
|
|
candidate.secondaryLabel = candidate.assetPath;
|
|
outCandidates.push_back(std::move(candidate));
|
|
}
|
|
}
|
|
|
|
inline void CollectSceneCandidatesRecursive(
|
|
::XCEngine::Components::GameObject* gameObject,
|
|
const ReferencePickerOptions& options,
|
|
std::vector<ReferencePickerCandidate>& outCandidates,
|
|
int depth = 0) {
|
|
if (!gameObject) {
|
|
return;
|
|
}
|
|
|
|
if (options.sceneFilter && options.sceneFilter(*gameObject)) {
|
|
ReferencePickerCandidate candidate;
|
|
candidate.label = gameObject->GetName();
|
|
candidate.secondaryLabel = BuildSceneHierarchyPath(gameObject);
|
|
candidate.normalizedSearchText =
|
|
NormalizeSearchText(candidate.label + " " + candidate.secondaryLabel);
|
|
candidate.iconKind = AssetIconKind::GameObject;
|
|
candidate.sceneObjectId = gameObject->GetID();
|
|
candidate.indent = static_cast<float>(depth) * 14.0f;
|
|
outCandidates.push_back(std::move(candidate));
|
|
}
|
|
|
|
for (::XCEngine::Components::GameObject* child : gameObject->GetChildren()) {
|
|
CollectSceneCandidatesRecursive(child, options, outCandidates, depth + 1);
|
|
}
|
|
}
|
|
|
|
inline bool MatchesSearch(const ReferencePickerCandidate& candidate, const SearchQuery& query) {
|
|
return query.Empty() || query.MatchesNormalized(candidate.normalizedSearchText);
|
|
}
|
|
|
|
inline bool DrawPickerTabButton(const char* label, bool active, ImVec2 size) {
|
|
const ImVec4 buttonColor = active ? ReferencePickerItemActiveColor() : ReferencePickerItemColor();
|
|
const ImVec4 hoverColor = ReferencePickerItemHoveredColor();
|
|
const ImVec4 activeColor = ReferencePickerItemActiveColor();
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, buttonColor);
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, hoverColor);
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, activeColor);
|
|
const bool pressed = ImGui::Button(label, size);
|
|
ImGui::PopStyleColor(3);
|
|
return pressed;
|
|
}
|
|
|
|
inline bool DrawPickerSearchField(char* buffer, size_t bufferSize, const char* hint) {
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, SearchFieldFramePadding());
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, SearchFieldFrameRounding());
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, ReferencePickerSurfaceColor());
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ReferencePickerItemHoveredColor());
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ReferencePickerItemActiveColor());
|
|
ImGui::SetNextItemWidth(-1.0f);
|
|
const bool changed = ImGui::InputTextWithHint("##ReferencePickerSearch", hint, buffer, bufferSize);
|
|
|
|
const ImVec2 min = ImGui::GetItemRectMin();
|
|
const ImVec2 max = ImGui::GetItemRectMax();
|
|
const ImVec2 center(min.x + SearchFieldFramePadding().x * 0.5f, (min.y + max.y) * 0.5f);
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
const ImU32 glyphColor = ImGui::GetColorU32(ReferencePickerTextDisabledColor());
|
|
drawList->AddCircle(center, 4.0f, glyphColor, 16, 1.5f);
|
|
drawList->AddLine(
|
|
ImVec2(center.x + 3.0f, center.y + 3.0f),
|
|
ImVec2(center.x + 7.0f, center.y + 7.0f),
|
|
glyphColor,
|
|
1.5f);
|
|
|
|
ImGui::PopStyleColor(3);
|
|
ImGui::PopStyleVar(2);
|
|
return changed;
|
|
}
|
|
|
|
inline bool DrawCurrentReferenceField(
|
|
const char* displayText,
|
|
const char* hint,
|
|
AssetIconKind iconKind,
|
|
float width,
|
|
bool hasValue,
|
|
bool* hoveredOut = nullptr) {
|
|
const float frameHeight = ImGui::GetFrameHeight();
|
|
const ImVec2 size((std::max)(width, 1.0f), frameHeight);
|
|
ImGui::InvisibleButton("##ReferencePickerField", size);
|
|
|
|
const bool hovered = ImGui::IsItemHovered();
|
|
const bool held = ImGui::IsItemActive();
|
|
if (hoveredOut) {
|
|
*hoveredOut = hovered;
|
|
}
|
|
|
|
const ImVec2 min = ImGui::GetItemRectMin();
|
|
const ImVec2 max = ImGui::GetItemRectMax();
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
drawList->AddRectFilled(
|
|
min,
|
|
max,
|
|
ImGui::GetColorU32(
|
|
held ? ImGuiCol_FrameBgActive :
|
|
hovered ? ImGuiCol_FrameBgHovered :
|
|
ImGuiCol_FrameBg),
|
|
ImGui::GetStyle().FrameRounding);
|
|
drawList->AddRect(
|
|
min,
|
|
max,
|
|
ImGui::GetColorU32(ImGuiCol_Border),
|
|
ImGui::GetStyle().FrameRounding);
|
|
|
|
const float iconSize = 16.0f;
|
|
const float iconMinX = min.x + 6.0f;
|
|
const float iconMinY = min.y + (frameHeight - iconSize) * 0.5f;
|
|
DrawAssetIcon(
|
|
drawList,
|
|
ImVec2(iconMinX, iconMinY),
|
|
ImVec2(iconMinX + iconSize, iconMinY + iconSize),
|
|
iconKind);
|
|
|
|
const char* text = hasValue ? displayText : hint;
|
|
const ImU32 textColor = ImGui::GetColorU32(hasValue ? ImGuiCol_Text : ImGuiCol_TextDisabled);
|
|
const ImVec2 textSize = ImGui::CalcTextSize(text ? text : "");
|
|
const float textX = iconMinX + iconSize + 6.0f;
|
|
const float textY = min.y + (frameHeight - textSize.y) * 0.5f;
|
|
drawList->PushClipRect(
|
|
ImVec2(textX, min.y),
|
|
ImVec2(max.x - 6.0f, max.y),
|
|
true);
|
|
drawList->AddText(ImVec2(textX, textY), textColor, text ? text : "");
|
|
drawList->PopClipRect();
|
|
|
|
return ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
|
}
|
|
|
|
inline bool DrawCandidateRow(
|
|
const char* id,
|
|
const char* label,
|
|
const char* secondaryLabel,
|
|
AssetIconKind iconKind,
|
|
bool selected,
|
|
float indent = 0.0f) {
|
|
const bool hasSecondary = secondaryLabel != nullptr && secondaryLabel[0] != '\0';
|
|
const float rowHeight = hasSecondary
|
|
? ImGui::GetTextLineHeight() * 2.0f + 10.0f
|
|
: ImGui::GetFrameHeight();
|
|
const ImVec2 rowSize((std::max)(ImGui::GetContentRegionAvail().x, 1.0f), rowHeight);
|
|
ImGui::InvisibleButton(id, rowSize);
|
|
|
|
const bool hovered = ImGui::IsItemHovered();
|
|
const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
|
|
|
const ImVec2 min = ImGui::GetItemRectMin();
|
|
const ImVec2 max = ImGui::GetItemRectMax();
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
if (selected || hovered) {
|
|
drawList->AddRectFilled(
|
|
min,
|
|
max,
|
|
ImGui::GetColorU32(selected ? ReferencePickerItemActiveColor() : ReferencePickerItemHoveredColor()),
|
|
3.0f);
|
|
}
|
|
|
|
const float iconSize = 16.0f;
|
|
const float iconMinX = min.x + 8.0f + indent;
|
|
const float iconMinY = min.y + (rowHeight - iconSize) * 0.5f;
|
|
DrawAssetIcon(
|
|
drawList,
|
|
ImVec2(iconMinX, iconMinY),
|
|
ImVec2(iconMinX + iconSize, iconMinY + iconSize),
|
|
iconKind);
|
|
|
|
const float textX = iconMinX + iconSize + 8.0f;
|
|
const float primaryY = min.y + (hasSecondary ? 3.0f : (rowHeight - ImGui::GetTextLineHeight()) * 0.5f);
|
|
const float secondaryY = primaryY + ImGui::GetTextLineHeight();
|
|
drawList->PushClipRect(ImVec2(textX, min.y), ImVec2(max.x - 6.0f, max.y), true);
|
|
drawList->AddText(ImVec2(textX, primaryY), ImGui::GetColorU32(ReferencePickerTextColor()), label ? label : "");
|
|
if (hasSecondary) {
|
|
drawList->AddText(
|
|
ImVec2(textX, secondaryY),
|
|
ImGui::GetColorU32(ReferencePickerTextDisabledColor()),
|
|
secondaryLabel);
|
|
}
|
|
drawList->PopClipRect();
|
|
|
|
return clicked;
|
|
}
|
|
|
|
inline bool DrawNoneRow(bool selected) {
|
|
const ImVec2 rowSize((std::max)(ImGui::GetContentRegionAvail().x, 1.0f), ImGui::GetFrameHeight());
|
|
ImGui::InvisibleButton("##ReferencePickerNone", rowSize);
|
|
|
|
const bool hovered = ImGui::IsItemHovered();
|
|
const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
|
const ImVec2 min = ImGui::GetItemRectMin();
|
|
const ImVec2 max = ImGui::GetItemRectMax();
|
|
if (selected || hovered) {
|
|
ImGui::GetWindowDrawList()->AddRectFilled(
|
|
min,
|
|
max,
|
|
ImGui::GetColorU32(selected ? ReferencePickerItemActiveColor() : ReferencePickerItemHoveredColor()),
|
|
3.0f);
|
|
}
|
|
|
|
const ImVec2 textSize = ImGui::CalcTextSize("None");
|
|
const float textY = min.y + (rowSize.y - textSize.y) * 0.5f;
|
|
ImGui::GetWindowDrawList()->AddText(
|
|
ImVec2(min.x + 8.0f, textY),
|
|
ImGui::GetColorU32(ReferencePickerTextDisabledColor()),
|
|
"None");
|
|
|
|
return clicked;
|
|
}
|
|
|
|
inline void EnsureValidActiveTab(ReferencePickerState& state, const ReferencePickerOptions& options) {
|
|
if (state.activeTab == ReferencePickerTab::Assets && options.showAssetsTab) {
|
|
return;
|
|
}
|
|
|
|
if (state.activeTab == ReferencePickerTab::Scene && options.showSceneTab) {
|
|
return;
|
|
}
|
|
|
|
state.activeTab = options.showAssetsTab ? ReferencePickerTab::Assets : ReferencePickerTab::Scene;
|
|
}
|
|
|
|
inline ReferencePickerTab ResolveDefaultTab(
|
|
const ReferencePickerOptions& options,
|
|
const std::string& currentAssetPath,
|
|
::XCEngine::Components::GameObject::ID currentSceneObjectId) {
|
|
if (!currentAssetPath.empty() && options.showAssetsTab) {
|
|
return ReferencePickerTab::Assets;
|
|
}
|
|
|
|
if (currentSceneObjectId != ::XCEngine::Components::GameObject::INVALID_ID && options.showSceneTab) {
|
|
return ReferencePickerTab::Scene;
|
|
}
|
|
|
|
return options.showAssetsTab ? ReferencePickerTab::Assets : ReferencePickerTab::Scene;
|
|
}
|
|
|
|
} // namespace Detail
|
|
|
|
inline ReferencePickerInteraction DrawReferencePickerControl(
|
|
const std::string& currentAssetPath,
|
|
::XCEngine::Components::GameObject::ID currentSceneObjectId,
|
|
const ReferencePickerOptions& options,
|
|
float width = -1.0f) {
|
|
ReferencePickerInteraction interaction;
|
|
constexpr const char* kProjectAssetPayloadType = "ASSET_ITEM";
|
|
|
|
ImGuiID pickerStateId = ImGui::GetID("##ReferencePickerState");
|
|
Detail::ReferencePickerState& state = Detail::GetReferencePickerState(pickerStateId);
|
|
Detail::EnsureValidActiveTab(state, options);
|
|
|
|
const float resolvedWidth = width > 0.0f ? width : ImGui::GetContentRegionAvail().x;
|
|
const float fieldWidth = (std::max)(resolvedWidth, 1.0f);
|
|
|
|
const bool hasAssetValue = !currentAssetPath.empty();
|
|
const bool hasSceneValue = currentSceneObjectId != ::XCEngine::Components::GameObject::INVALID_ID;
|
|
|
|
std::string currentDisplayText;
|
|
std::string currentTooltip;
|
|
AssetIconKind currentIconKind = AssetIconKind::File;
|
|
if (hasAssetValue) {
|
|
currentDisplayText = Detail::GetAssetDisplayNameFromPath(currentAssetPath);
|
|
currentTooltip = currentAssetPath;
|
|
currentIconKind = Detail::ResolveAssetIconKindFromPath(currentAssetPath);
|
|
} else if (hasSceneValue) {
|
|
auto& editorContext = Application::Get().GetEditorContext();
|
|
if (::XCEngine::Components::GameObject* gameObject =
|
|
editorContext.GetSceneManager().GetEntity(currentSceneObjectId)) {
|
|
currentDisplayText = gameObject->GetName();
|
|
currentTooltip = Detail::BuildSceneHierarchyPath(gameObject);
|
|
} else {
|
|
currentDisplayText = "Missing";
|
|
}
|
|
currentIconKind = AssetIconKind::GameObject;
|
|
}
|
|
|
|
bool fieldHovered = false;
|
|
const bool fieldClicked = Detail::DrawCurrentReferenceField(
|
|
currentDisplayText.c_str(),
|
|
options.emptyHint,
|
|
currentIconKind,
|
|
fieldWidth,
|
|
hasAssetValue || hasSceneValue,
|
|
&fieldHovered);
|
|
const ImVec2 fieldMin = ImGui::GetItemRectMin();
|
|
const ImVec2 fieldMax = ImGui::GetItemRectMax();
|
|
|
|
if (fieldHovered && !currentTooltip.empty()) {
|
|
ImGui::SetTooltip("%s", currentTooltip.c_str());
|
|
}
|
|
|
|
if (ImGui::BeginDragDropTarget()) {
|
|
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(
|
|
kProjectAssetPayloadType,
|
|
ImGuiDragDropFlags_AcceptNoDrawDefaultRect)) {
|
|
if (payload->Data != nullptr) {
|
|
const std::string droppedPath(static_cast<const char*>(payload->Data));
|
|
std::string extensionLower = std::filesystem::path(droppedPath).extension().string();
|
|
std::transform(extensionLower.begin(), extensionLower.end(), extensionLower.begin(), [](unsigned char ch) {
|
|
return static_cast<char>(std::tolower(ch));
|
|
});
|
|
if (Detail::MatchesSupportedAssetExtension(extensionLower, options.supportedAssetExtensions)) {
|
|
const auto& editorContext = Application::Get().GetEditorContext();
|
|
interaction.assignedAssetPath = editorContext.GetProjectPath().empty()
|
|
? droppedPath
|
|
: ProjectFileUtils::MakeProjectRelativePath(editorContext.GetProjectPath(), droppedPath);
|
|
}
|
|
}
|
|
}
|
|
ImGui::EndDragDropTarget();
|
|
}
|
|
|
|
const char* popupId = "##ReferencePickerPopup";
|
|
if (fieldClicked) {
|
|
state.activeTab = Detail::ResolveDefaultTab(options, currentAssetPath, currentSceneObjectId);
|
|
state.searchBuffer[0] = '\0';
|
|
ImGui::OpenPopup(popupId);
|
|
}
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(fieldMin.x, fieldMax.y + 2.0f), ImGuiCond_Appearing);
|
|
ImGui::SetNextWindowSize(ImVec2((std::max)(fieldWidth, 320.0f), 360.0f), ImGuiCond_Appearing);
|
|
if (!Detail::BeginReferencePickerPopup(popupId, options.popupTitle)) {
|
|
return interaction;
|
|
}
|
|
|
|
Detail::EnsureValidActiveTab(state, options);
|
|
const bool showTabs = options.showAssetsTab && options.showSceneTab;
|
|
if (showTabs) {
|
|
const float availableWidth = ImGui::GetContentRegionAvail().x;
|
|
const float tabWidth = (availableWidth - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
|
if (Detail::DrawPickerTabButton(
|
|
options.assetsTabLabel,
|
|
state.activeTab == ReferencePickerTab::Assets,
|
|
ImVec2(tabWidth, 0.0f))) {
|
|
state.activeTab = ReferencePickerTab::Assets;
|
|
}
|
|
ImGui::SameLine();
|
|
if (Detail::DrawPickerTabButton(
|
|
options.sceneTabLabel,
|
|
state.activeTab == ReferencePickerTab::Scene,
|
|
ImVec2(tabWidth, 0.0f))) {
|
|
state.activeTab = ReferencePickerTab::Scene;
|
|
}
|
|
ImGui::Spacing();
|
|
}
|
|
|
|
Detail::DrawPickerSearchField(state.searchBuffer.data(), state.searchBuffer.size(), options.searchHint);
|
|
ImGui::Spacing();
|
|
|
|
std::vector<Detail::ReferencePickerCandidate> candidates;
|
|
candidates.reserve(64);
|
|
|
|
auto& editorContext = Application::Get().GetEditorContext();
|
|
if (state.activeTab == ReferencePickerTab::Assets) {
|
|
if (options.showAssetsTab) {
|
|
Detail::CollectAssetCandidatesRecursive(
|
|
editorContext.GetProjectManager().GetRootFolder(),
|
|
editorContext.GetProjectPath(),
|
|
options,
|
|
candidates);
|
|
}
|
|
} else {
|
|
if (options.showSceneTab) {
|
|
const auto& roots = editorContext.GetSceneManager().GetRootEntities();
|
|
for (::XCEngine::Components::GameObject* root : roots) {
|
|
Detail::CollectSceneCandidatesRecursive(root, options, candidates);
|
|
}
|
|
}
|
|
}
|
|
|
|
const SearchQuery searchQuery(state.searchBuffer.data());
|
|
ImGui::BeginChild("##ReferencePickerList", ImVec2(0.0f, 0.0f), true);
|
|
|
|
const bool noneSelected = !hasAssetValue && !hasSceneValue;
|
|
if (Detail::DrawNoneRow(noneSelected)) {
|
|
interaction.clearRequested = true;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
|
|
bool anyVisibleCandidate = false;
|
|
for (size_t index = 0; index < candidates.size(); ++index) {
|
|
const Detail::ReferencePickerCandidate& candidate = candidates[index];
|
|
if (!Detail::MatchesSearch(candidate, searchQuery)) {
|
|
continue;
|
|
}
|
|
|
|
anyVisibleCandidate = true;
|
|
ImGui::PushID(static_cast<int>(index));
|
|
const bool selected = candidate.IsAsset()
|
|
? candidate.assetPath == currentAssetPath
|
|
: candidate.sceneObjectId == currentSceneObjectId;
|
|
const bool clicked = Detail::DrawCandidateRow(
|
|
"##ReferencePickerCandidate",
|
|
candidate.label.c_str(),
|
|
candidate.secondaryLabel.c_str(),
|
|
candidate.iconKind,
|
|
selected,
|
|
candidate.indent);
|
|
ImGui::PopID();
|
|
|
|
if (!clicked) {
|
|
continue;
|
|
}
|
|
|
|
if (candidate.IsAsset()) {
|
|
interaction.assignedAssetPath = candidate.assetPath;
|
|
} else if (candidate.IsSceneObject()) {
|
|
interaction.assignedSceneObjectId = candidate.sceneObjectId;
|
|
}
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
|
|
if (!anyVisibleCandidate) {
|
|
ImGui::TextColored(
|
|
Detail::ReferencePickerTextDisabledColor(),
|
|
"%s",
|
|
state.activeTab == ReferencePickerTab::Assets
|
|
? options.noAssetsText
|
|
: options.noSceneText);
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
Detail::EndReferencePickerPopup();
|
|
return interaction;
|
|
}
|
|
|
|
} // namespace UI
|
|
} // namespace Editor
|
|
} // namespace XCEngine
|