Skip to content

Commit 964f16e

Browse files
authored
feat: implement drag-and-drop for entities and external files (#342)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added drag-and-drop support for assets and files in the editor, including the Asset Manager, Scene Tree, and Editor Scene windows. * Users can now import files by dragging them into the editor, and rearrange assets and scene objects using drag-and-drop. * Visual indicators and overlays are displayed during drag-and-drop operations for improved usability. * **Improvements** * Asset paths are now automatically normalized for consistency. * Asset metadata can now be edited directly. * Enhanced asset location string formatting and handling. * Streamlined folder structure handling in the Asset Manager. * Toolbar alignment in the Editor Scene window is now more precise. * **Bug Fixes** * Corrected asset grid filtering to ensure accurate folder content display. * **Other** * Undo/redo actions now support entity parent changes in the scene hierarchy. * Internal support for file drop events added to the application and window system. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 010736a + 24761b9 commit 964f16e

33 files changed

Lines changed: 1186 additions & 177 deletions

common/Path.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//// Path.cpp ///////////////////////////////////////////////////////////////
2+
//
3+
// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz
4+
// zzzzzzz zzz zzzz zzzz zzzz zzzz
5+
// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz
6+
// zzz zzz zzz z zzzz zzzz zzzz zzzz
7+
// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz
8+
//
9+
// Author: Mehdy MORVAN
10+
// Date: 24/07/2025
11+
// Description: Source file for the path utilities
12+
//
13+
///////////////////////////////////////////////////////////////////////////////
14+
15+
#include "Path.hpp"
16+
17+
namespace nexo {
18+
19+
const std::filesystem::path& Path::getExecutablePath()
20+
{
21+
if (!m_executablePathCached.empty() && !m_executableRootPathCached.empty())
22+
return m_executablePathCached;
23+
const boost::dll::fs::path path = boost::dll::program_location();
24+
m_executablePathCached = path.c_str();
25+
m_executableRootPathCached = m_executablePathCached.parent_path();
26+
return m_executablePathCached;
27+
}
28+
29+
std::filesystem::path Path::resolvePathRelativeToExe(const std::filesystem::path& path)
30+
{
31+
if (m_executableRootPathCached.empty())
32+
getExecutablePath();
33+
return (m_executableRootPathCached / path).lexically_normal();
34+
}
35+
36+
void Path::resetCache()
37+
{
38+
m_executablePathCached.clear();
39+
m_executableRootPathCached.clear();
40+
}
41+
42+
std::string normalizePathAndRemovePrefixSlash(const std::string &rawPath)
43+
{
44+
namespace fs = std::filesystem;
45+
fs::path p = fs::path(rawPath).lexically_normal();
46+
47+
std::string s = p.generic_string();
48+
49+
if (s == "/" || s.empty())
50+
return {};
51+
52+
size_t start = s.find_first_not_of('/');
53+
size_t end = s.find_last_not_of('/');
54+
return s.substr(start, end - start + 1);
55+
}
56+
}

common/Path.hpp

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,7 @@ namespace nexo {
2828
* @brief Get the path to the executable (e.g.: nexoEditor)
2929
* @return The path to the executable
3030
*/
31-
static const std::filesystem::path& getExecutablePath()
32-
{
33-
if (!m_executablePathCached.empty() && !m_executableRootPathCached.empty())
34-
return m_executablePathCached;
35-
const boost::dll::fs::path path = boost::dll::program_location();
36-
m_executablePathCached = path.c_str();
37-
m_executableRootPathCached = m_executablePathCached.parent_path();
38-
return m_executablePathCached;
39-
}
31+
static const std::filesystem::path& getExecutablePath();
4032

4133
/**
4234
* @brief Resolve a path relative to the executable
@@ -46,21 +38,12 @@ namespace nexo {
4638
* @note Example: if assets is a folder in the same directory as the executable, you can use: resolvePathRelativeToExe("assets")
4739
* @example ../editor/src/Editor.cpp
4840
*/
49-
static std::filesystem::path resolvePathRelativeToExe(const std::filesystem::path& path)
50-
{
51-
if (m_executableRootPathCached.empty())
52-
getExecutablePath();
53-
return (m_executableRootPathCached / path).lexically_normal();
54-
}
41+
static std::filesystem::path resolvePathRelativeToExe(const std::filesystem::path& path);
5542

5643
/**
5744
* @brief Reset the cached paths
5845
*/
59-
static void resetCache()
60-
{
61-
m_executablePathCached.clear();
62-
m_executableRootPathCached.clear();
63-
}
46+
static void resetCache();
6447

6548
private:
6649
Path() = default;
@@ -69,5 +52,5 @@ namespace nexo {
6952
inline static std::filesystem::path m_executableRootPathCached;
7053
};
7154

72-
55+
std::string normalizePathAndRemovePrefixSlash(const std::string &rawPath);
7356
} // namespace nexo

editor/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ set(SRCS
1111
common/Exception.cpp
1212
common/math/Matrix.cpp
1313
common/math/Light.cpp
14+
common/Path.cpp
1415
editor/main.cpp
1516
editor/src/backends/ImGuiBackend.cpp
1617
editor/src/backends/opengl/openglImGuiBackend.cpp
@@ -47,12 +48,14 @@ set(SRCS
4748
editor/src/DocumentWindows/EditorScene/Shutdown.cpp
4849
editor/src/DocumentWindows/EditorScene/Toolbar.cpp
4950
editor/src/DocumentWindows/EditorScene/Update.cpp
51+
editor/src/DocumentWindows/EditorScene/DragDrop.cpp
5052
editor/src/DocumentWindows/AssetManager/Init.cpp
5153
editor/src/DocumentWindows/AssetManager/Show.cpp
5254
editor/src/DocumentWindows/AssetManager/Shutdown.cpp
5355
editor/src/DocumentWindows/AssetManager/Update.cpp
5456
editor/src/DocumentWindows/AssetManager/FolderTree.cpp
5557
editor/src/DocumentWindows/AssetManager/Thumbnail.cpp
58+
editor/src/DocumentWindows/AssetManager/FileDrop.cpp
5659
editor/src/DocumentWindows/ConsoleWindow/Init.cpp
5760
editor/src/DocumentWindows/ConsoleWindow/Log.cpp
5861
editor/src/DocumentWindows/ConsoleWindow/Show.cpp
@@ -77,6 +80,7 @@ set(SRCS
7780
editor/src/DocumentWindows/SceneTreeWindow/Shutdown.cpp
7881
editor/src/DocumentWindows/SceneTreeWindow/Update.cpp
7982
editor/src/DocumentWindows/SceneTreeWindow/Shortcuts.cpp
83+
editor/src/DocumentWindows/SceneTreeWindow/DragDrop.cpp
8084
editor/src/DocumentWindows/PopupManager.cpp
8185
editor/src/DocumentWindows/EntityProperties/TransformProperty.cpp
8286
editor/src/DocumentWindows/EntityProperties/RenderProperty.cpp

editor/src/DocumentWindows/AssetManager/AssetManagerWindow.hpp

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
#include <imgui.h>
1919
#include <assets/AssetRef.hpp>
2020
#include "utils/TransparentStringHash.hpp"
21+
#include <core/event/WindowEvent.hpp>
22+
#include "assets/Asset.hpp"
2123

2224
namespace nexo::editor {
2325

24-
class AssetManagerWindow final : public ADocumentWindow {
26+
class AssetManagerWindow final : public ADocumentWindow, LISTENS_TO(event::EventFileDrop) {
2527
public:
2628
using ADocumentWindow::ADocumentWindow;
2729

@@ -30,6 +32,8 @@ namespace nexo::editor {
3032
void show() override;
3133
void update() override;
3234

35+
void handleEvent(event::EventFileDrop& event) override;
36+
3337
private:
3438
struct LayoutSettings {
3539
struct LayoutSizes {
@@ -74,7 +78,8 @@ namespace nexo::editor {
7478
void handleSelection(int index, bool isSelected);
7579

7680
assets::AssetType m_selectedType = assets::AssetType::UNKNOWN;
77-
std::string m_currentFolder;
81+
std::string m_currentFolder; // Currently selected folder
82+
std::string m_hoveredFolder; // Currently hovered folder
7883
std::vector<std::pair<std::string, std::string>> m_folderStructure; // Pairs of (path, name)
7984
char m_searchBuffer[256] = "";
8085

@@ -110,5 +115,25 @@ namespace nexo::editor {
110115
const ImVec2& itemPos,
111116
const ImVec2& itemSize
112117
);
118+
119+
std::vector<std::string> m_pendingDroppedFiles;
120+
bool m_showDropIndicator = false;
121+
122+
void handleDroppedFiles();
123+
const assets::AssetLocation getAssetLocation(const std::filesystem::path &path) const;
124+
void importDroppedFile(const std::string& filePath);
125+
};
126+
127+
/**
128+
* @brief Payload structure for drag and drop operations from asset manager.
129+
*
130+
* Contains information about the asset being dragged.
131+
*/
132+
struct AssetDragDropPayload
133+
{
134+
assets::AssetType type; ///< Type of the asset
135+
assets::AssetID id; ///< ID of the asset
136+
std::string path; ///< Path to the asset
137+
std::string name; ///< Display name of the asset
113138
};
114139
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//// FileDrop.cpp /////////////////////////////////////////////////////////////
2+
//
3+
// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz
4+
// zzzzzzz zzz zzzz zzzz zzzz zzzz
5+
// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz
6+
// zzz zzz zzz z zzzz zzzz zzzz zzzz
7+
// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz
8+
//
9+
// Author: Jean CARDONNE
10+
// Date: 30/06/2025
11+
// Description: Implementation of file drop handling for asset manager
12+
//
13+
///////////////////////////////////////////////////////////////////////////////
14+
15+
#include "AssetManagerWindow.hpp"
16+
#include "assets/Asset.hpp"
17+
#include "assets/AssetImporter.hpp"
18+
#include "assets/AssetLocation.hpp"
19+
#include "assets/Assets/Model/Model.hpp"
20+
#include "assets/Assets/Texture/Texture.hpp"
21+
#include "Logger.hpp"
22+
#include <filesystem>
23+
#include <algorithm>
24+
25+
namespace nexo::editor {
26+
27+
static assets::AssetType getAssetTypeFromExtension(const std::string &extension)
28+
{
29+
static const std::set<std::string> imageExtensions = {
30+
".png", ".jpg", ".jpeg", ".bmp", ".tga", ".gif", ".psd", ".hdr", ".pic", ".pnm", ".ppm", ".pgm"
31+
};
32+
if (imageExtensions.contains(extension))
33+
return assets::AssetType::TEXTURE;
34+
static const std::set<std::string> modelExtensions = {
35+
".gltf", ".glb", ".fbx", ".obj", ".dae", ".3ds", ".stl", ".ply", ".blend", ".x3d", ".ifc"
36+
};
37+
if (modelExtensions.contains(extension))
38+
return assets::AssetType::MODEL;
39+
return assets::AssetType::UNKNOWN;
40+
}
41+
42+
const assets::AssetLocation AssetManagerWindow::getAssetLocation(const std::filesystem::path &path) const
43+
{
44+
std::string assetName = path.stem().string();
45+
std::filesystem::path folderPath;
46+
std::string targetFolder = !m_hoveredFolder.empty() ? m_hoveredFolder : m_currentFolder;
47+
48+
std::string assetPath = targetFolder;
49+
std::string locationString = assetName + "@" + assetPath;
50+
51+
LOG(NEXO_DEV,
52+
"Creating asset location: {} (current folder: '{}', hovered: '{}')",
53+
locationString,
54+
m_currentFolder,
55+
m_hoveredFolder);
56+
57+
assets::AssetLocation location(locationString);
58+
return location;
59+
}
60+
61+
void AssetManagerWindow::handleEvent(event::EventFileDrop& event)
62+
{
63+
m_pendingDroppedFiles.insert(m_pendingDroppedFiles.end(),
64+
event.files.begin(),
65+
event.files.end());
66+
}
67+
68+
void AssetManagerWindow::handleDroppedFiles()
69+
{
70+
if (m_pendingDroppedFiles.empty())
71+
return;
72+
73+
for (const auto& filePath : m_pendingDroppedFiles)
74+
importDroppedFile(filePath);
75+
m_pendingDroppedFiles.clear();
76+
77+
m_folderStructure.clear();
78+
buildFolderStructure();
79+
}
80+
81+
void AssetManagerWindow::importDroppedFile(const std::string& filePath)
82+
{
83+
std::filesystem::path path(filePath);
84+
85+
if (!std::filesystem::exists(path)) {
86+
LOG(NEXO_WARN, "Dropped file does not exist: {}", filePath);
87+
return;
88+
}
89+
90+
std::string extension = path.extension().string();
91+
std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
92+
93+
assets::AssetType assetType = getAssetTypeFromExtension(extension);
94+
if (assetType == assets::AssetType::UNKNOWN) {
95+
LOG(NEXO_WARN, "Unsupported file type: {}", extension);
96+
return;
97+
}
98+
99+
assets::AssetLocation location = getAssetLocation(path);
100+
101+
assets::AssetImporter importer;
102+
assets::ImporterFileInput fileInput{path};
103+
try {
104+
auto assetRef = importer.importAssetAuto(location, fileInput);
105+
if (!assetRef)
106+
LOG(NEXO_ERROR, "Failed to import asset: {}", location.getPath().data());
107+
} catch (const std::exception& e) {
108+
LOG(NEXO_ERROR, "Exception while importing {}: {}", location.getPath().data(), e.what());
109+
}
110+
}
111+
}

editor/src/DocumentWindows/AssetManager/FolderTree.cpp

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -155,51 +155,48 @@ namespace nexo::editor {
155155
void AssetManagerWindow::buildFolderStructure()
156156
{
157157
m_folderStructure.clear();
158+
// Root entry
158159
m_folderStructure.emplace_back("", "Assets");
159160
m_folderChildren.clear(); // Clear the folder children map
160161

161162
// First pass: build the folder structure
162163
std::set<std::string, std::less<>> uniqueFolderPaths;
163164

165+
std::unordered_set<std::string> seen{""};
166+
164167
const auto assets = assets::AssetCatalog::getInstance().getAssets();
165-
for (const auto& asset : assets) {
166-
if (auto assetData = asset.lock()) {
167-
std::string fullPath = assetData->getMetadata().location.getPath();
168-
std::filesystem::path fsPath(fullPath);
169-
170-
// Extract all parent directories from the path
171-
while (fsPath.has_parent_path()) {
172-
fsPath = fsPath.parent_path();
173-
if (!fsPath.empty()) {
174-
uniqueFolderPaths.insert(fsPath.string());
168+
for (auto& ref : assets) {
169+
if (auto assetData = ref.lock()) {
170+
// normalized path: e.g. "Random/Sub"
171+
std::filesystem::path p{ assetData->getMetadata().location.getPath() };
172+
std::filesystem::path curr;
173+
for (auto const& part : p) {
174+
// skip empty or “_internal” style parts
175+
auto s = part.string();
176+
if (s.empty() || s.front() == '_')
177+
continue;
178+
curr /= part;
179+
auto folderPath = curr.string();
180+
if (seen.emplace(folderPath).second) {
181+
m_folderStructure.emplace_back(
182+
folderPath,
183+
curr.filename().string()
184+
);
175185
}
176186
}
177187
}
178188
}
179189

180-
// Add the unique folder paths to m_folderStructure
181-
for (const auto& folderPath : uniqueFolderPaths) {
182-
std::filesystem::path fsPath(folderPath);
183-
std::string folderName = fsPath.filename().string();
184-
m_folderStructure.emplace_back(folderPath, folderName);
185-
}
186-
187-
// Second pass: build the parent-child map
188-
for (const auto& [path, name] : m_folderStructure) {
189-
if (path.empty()) continue; // Skip root
190-
191-
std::filesystem::path fsPath(path);
192-
std::string parentPath = fsPath.parent_path().string();
193-
194-
// If parent path is empty, set it to "" (root)
195-
if (parentPath.empty()) {
196-
parentPath = "";
190+
std::sort(
191+
m_folderStructure.begin() + 1,
192+
m_folderStructure.end(),
193+
[](auto const& a, auto const& b){
194+
return a.first < b.first;
197195
}
198-
199-
m_folderChildren[parentPath].push_back(path);
200-
}
196+
);
201197
}
202198

199+
203200
void AssetManagerWindow::drawFolderTree()
204201
{
205202
handleNewFolderCreation();

0 commit comments

Comments
 (0)