1386 lines
40 KiB
C++
1386 lines
40 KiB
C++
#include <imgui/imgui.h>
|
|
|
|
#include "asset_browser.h"
|
|
#include "editor/asset_compiler.h"
|
|
#include "editor/prefab_system.h"
|
|
#include "editor/render_interface.h"
|
|
#include "editor/settings.h"
|
|
#include "editor/studio_app.h"
|
|
#include "editor/utils.h"
|
|
#include "editor/world_editor.h"
|
|
#include "engine/crt.h"
|
|
#include "engine/engine.h"
|
|
#include "engine/hash.h"
|
|
#include "engine/log.h"
|
|
#include "engine/os.h"
|
|
#include "engine/path.h"
|
|
#include "engine/profiler.h"
|
|
#include "engine/reflection.h"
|
|
#include "engine/resource.h"
|
|
#include "engine/resource_manager.h"
|
|
#include "engine/string.h"
|
|
#include "engine/world.h"
|
|
#include "utils.h"
|
|
|
|
|
|
namespace Lumix {
|
|
|
|
|
|
static void clampText(char* text, int width)
|
|
{
|
|
char* end = text + stringLength(text);
|
|
ImVec2 size = ImGui::CalcTextSize(text);
|
|
if (size.x <= width) return;
|
|
|
|
do
|
|
{
|
|
*(end - 1) = '\0';
|
|
*(end - 2) = '.';
|
|
*(end - 3) = '.';
|
|
*(end - 4) = '.';
|
|
--end;
|
|
|
|
size = ImGui::CalcTextSize(text);
|
|
} while (size.x > width && end - text > 4);
|
|
}
|
|
|
|
static const char* getResourceFilePath(const char* str)
|
|
{
|
|
const char* c = str;
|
|
while (*c && *c != ':') ++c;
|
|
return *c != ':' ? str : c + 1;
|
|
}
|
|
|
|
static Span<const char> getSubresource(const char* str)
|
|
{
|
|
Span<const char> ret;
|
|
ret.m_begin = str;
|
|
ret.m_end = str;
|
|
while(*ret.m_end && *ret.m_end != ':') ++ret.m_end;
|
|
return ret;
|
|
}
|
|
|
|
bool AssetBrowser::Plugin::createTile(const char* in_path, const char* out_path, ResourceType type)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void AssetBrowser::Plugin::gui(Span<Resource*> resources) {
|
|
bool waiting = false;
|
|
for (const Resource* r : resources) {
|
|
if (r->isEmpty()) {
|
|
ImGui::TextUnformatted("Waiting for load...");
|
|
waiting = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!waiting) {
|
|
RuntimeHash hash = RuntimeHash(resources.begin(), resources.length() * sizeof(resources[0]));
|
|
if (hash != m_current_hash) {
|
|
// we remember undo only for currently selected resources
|
|
m_current_hash = hash;
|
|
clearUndoStack();
|
|
m_defer_push_undo = true;
|
|
}
|
|
|
|
if (onGUI(resources)) {
|
|
m_defer_push_undo = true;
|
|
}
|
|
else if (m_defer_push_undo) {
|
|
pushUndo(SimpleUndoRedo::NO_MERGE_UNDO);
|
|
m_defer_push_undo = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AssetBrowserImpl : AssetBrowser {
|
|
struct FileInfo {
|
|
StaticString<LUMIX_MAX_PATH> clamped_filename;
|
|
StaticString<LUMIX_MAX_PATH> filepath;
|
|
FilePathHash file_path_hash;
|
|
void* tex = nullptr;
|
|
bool create_called = false;
|
|
};
|
|
|
|
struct ImmediateTile : FileInfo {
|
|
u32 gc_counter;
|
|
};
|
|
|
|
AssetBrowserImpl(StudioApp& app)
|
|
: m_selected_resources(app.getAllocator())
|
|
, m_history(app.getAllocator())
|
|
, m_dir_history(app.getAllocator())
|
|
, m_plugins(app.getAllocator())
|
|
, m_app(app)
|
|
, m_is_open(false)
|
|
, m_show_thumbnails(true)
|
|
, m_show_subresources(true)
|
|
, m_file_infos(app.getAllocator())
|
|
, m_immediate_tiles(app.getAllocator())
|
|
, m_subdirs(app.getAllocator())
|
|
{
|
|
m_filter[0] = '\0';
|
|
|
|
onBasePathChanged();
|
|
|
|
m_back_action.init("Back", "Back in asset history", "back", ICON_FA_ARROW_LEFT, false);
|
|
m_back_action.func.bind<&AssetBrowserImpl::goBack>(this);
|
|
m_forward_action.init("Forward", "Forward in asset history", "forward", ICON_FA_ARROW_RIGHT, false);
|
|
m_forward_action.func.bind<&AssetBrowserImpl::goForward>(this);
|
|
|
|
m_toggle_ui.init("Asset browser", "Toggle Asset Browser UI", "asset_browser", "", false);
|
|
m_toggle_ui.func.bind<&AssetBrowserImpl::toggleUI>(this);
|
|
m_toggle_ui.is_selected.bind<&AssetBrowserImpl::isOpen>(this);
|
|
|
|
m_undo_action.init(ICON_FA_UNDO "Undo", "Asset browser undo", "asset_browser_undo", ICON_FA_UNDO, os::Keycode::Z, Action::Modifiers::CTRL, true);
|
|
m_undo_action.func.bind<&AssetBrowserImpl::undo>(this);
|
|
m_undo_action.plugin = this;
|
|
|
|
m_redo_action.init(ICON_FA_REDO "Redo", "Asset browser redo", "asset_browser_redo", ICON_FA_UNDO, os::Keycode::Z, Action::Modifiers::CTRL | Action::Modifiers::SHIFT, true);
|
|
m_redo_action.func.bind<&AssetBrowserImpl::redo>(this);
|
|
m_redo_action.plugin = this;
|
|
|
|
m_app.addAction(&m_undo_action);
|
|
m_app.addAction(&m_redo_action);
|
|
m_app.addAction(&m_back_action);
|
|
m_app.addAction(&m_forward_action);
|
|
m_app.addWindowAction(&m_toggle_ui);
|
|
}
|
|
|
|
void onBasePathChanged() {
|
|
const char* base_path = m_app.getEngine().getFileSystem().getBasePath();
|
|
Path path(base_path, ".lumix");
|
|
bool success = os::makePath(path);
|
|
path.append("/asset_tiles");
|
|
success = os::makePath(path) && success;
|
|
if (!success) logError("Could not create ", path);
|
|
}
|
|
|
|
void releaseResources() override {
|
|
unloadResources();
|
|
RenderInterface* ri = m_app.getRenderInterface();
|
|
for (FileInfo& info : m_file_infos) {
|
|
ri->unloadTexture(info.tex);
|
|
}
|
|
m_file_infos.clear();
|
|
for (FileInfo& info : m_immediate_tiles) {
|
|
ri->unloadTexture(info.tex);
|
|
}
|
|
m_immediate_tiles.clear();
|
|
}
|
|
|
|
|
|
~AssetBrowserImpl() override
|
|
{
|
|
m_app.removeAction(&m_undo_action);
|
|
m_app.removeAction(&m_redo_action);
|
|
m_app.removeAction(&m_toggle_ui);
|
|
m_app.removeAction(&m_back_action);
|
|
m_app.removeAction(&m_forward_action);
|
|
m_app.getAssetCompiler().listChanged().unbind<&AssetBrowserImpl::onResourceListChanged>(this);
|
|
m_app.getAssetCompiler().resourceCompiled().unbind<&AssetBrowserImpl::onResourceCompiled>(this);
|
|
|
|
ASSERT(m_plugins.size() == 0);
|
|
}
|
|
|
|
void onInitFinished() override {
|
|
m_app.getAssetCompiler().listChanged().bind<&AssetBrowserImpl::onResourceListChanged>(this);
|
|
m_app.getAssetCompiler().resourceCompiled().bind<&AssetBrowserImpl::onResourceCompiled>(this);
|
|
}
|
|
|
|
void onResourceCompiled(Resource& resource) {
|
|
Span<const char> dir = Path::getDir(resource.getPath().c_str());
|
|
if (!Path::isSame(dir, Span<const char>(m_dir, (u32)strlen(m_dir)))) return;
|
|
|
|
RenderInterface* ri = m_app.getRenderInterface();
|
|
Engine& engine = m_app.getEngine();
|
|
FileSystem& fs = engine.getFileSystem();
|
|
|
|
for (i32 i = 0; i < m_file_infos.size(); ++i) {
|
|
FileInfo& info = m_file_infos[i];
|
|
if (info.filepath != resource.getPath().c_str()) continue;
|
|
|
|
switch (getState(info, fs)) {
|
|
case TileState::DELETED:
|
|
ri->unloadTexture(info.tex);
|
|
info.create_called = false;
|
|
m_file_infos.erase(i);
|
|
return;
|
|
case TileState::NOT_CREATED:
|
|
case TileState::OUTDATED:
|
|
ri->unloadTexture(info.tex);
|
|
info.create_called = false;
|
|
info.tex = nullptr;
|
|
return;
|
|
case TileState::OK: return;
|
|
}
|
|
}
|
|
|
|
addTile(resource.getPath());
|
|
sortTiles();
|
|
}
|
|
|
|
void onResourceListChanged(const Path& path) {
|
|
Engine& engine = m_app.getEngine();
|
|
FileSystem& fs = engine.getFileSystem();
|
|
const Path fullpath(fs.getBasePath(), path.c_str());
|
|
if (os::dirExists(fullpath)) {
|
|
changeDir(m_dir, false);
|
|
return;
|
|
}
|
|
|
|
Span<const char> dir = Path::getDir(path.c_str());
|
|
if (!Path::isSame(dir, Span<const char>(m_dir, (u32)strlen(m_dir)))) return;
|
|
|
|
RenderInterface* ri = m_app.getRenderInterface();
|
|
|
|
for (i32 i = 0; i < m_file_infos.size(); ++i) {
|
|
FileInfo& info = m_file_infos[i];
|
|
if (info.filepath != path.c_str()) continue;
|
|
|
|
switch (getState(info, fs)) {
|
|
case TileState::DELETED:
|
|
ri->unloadTexture(info.tex);
|
|
info.create_called = false;
|
|
m_file_infos.erase(i);
|
|
return;
|
|
case TileState::NOT_CREATED:
|
|
case TileState::OUTDATED:
|
|
ri->unloadTexture(info.tex);
|
|
info.create_called = false;
|
|
info.tex = nullptr;
|
|
return;
|
|
case TileState::OK: return;
|
|
}
|
|
}
|
|
|
|
addTile(path);
|
|
sortTiles();
|
|
}
|
|
|
|
void unloadResources()
|
|
{
|
|
if (m_selected_resources.empty()) return;
|
|
|
|
for (Resource* res : m_selected_resources) {
|
|
for (auto* plugin : m_plugins) {
|
|
plugin->onResourceUnloaded(res);
|
|
}
|
|
res->decRefCount();
|
|
}
|
|
|
|
m_selected_resources.clear();
|
|
}
|
|
|
|
bool hasFocus() override { return m_has_focus; }
|
|
|
|
void update(float) override
|
|
{
|
|
PROFILE_FUNCTION();
|
|
|
|
RenderInterface* ri = m_app.getRenderInterface();
|
|
for (i32 i = m_immediate_tiles.size() - 1; i >= 0; --i) {
|
|
u32& counter = m_immediate_tiles[i].gc_counter;
|
|
--counter;
|
|
if (counter == 0) {
|
|
ri->unloadTexture(m_immediate_tiles[i].tex);
|
|
m_immediate_tiles.swapAndPop(i);
|
|
}
|
|
}
|
|
|
|
for (auto* plugin : m_plugins) plugin->update();
|
|
}
|
|
|
|
void addTile(const Path& path) {
|
|
if (!m_show_subresources && contains(path.c_str(), ':')) return;
|
|
if (m_filter[0] && !stristr(path.c_str(), m_filter)) return;
|
|
|
|
FileInfo tile;
|
|
char filename[LUMIX_MAX_PATH];
|
|
Span<const char> subres = getSubresource(path.c_str());
|
|
if (*subres.end()) {
|
|
copyNString(Span(filename), subres.begin(), subres.length());
|
|
catString(filename, ":");
|
|
catString(Span(filename), Path::getBasename(path.c_str()));
|
|
}
|
|
else {
|
|
copyString(Span(filename), Path::getBasename(path.c_str()));
|
|
}
|
|
clampText(filename, int(TILE_SIZE * m_thumbnail_size));
|
|
|
|
tile.file_path_hash = path.getHash();
|
|
tile.filepath = path.c_str();
|
|
tile.clamped_filename = filename;
|
|
|
|
m_file_infos.push(tile);
|
|
}
|
|
|
|
void changeDir(const char* path, bool push_history)
|
|
{
|
|
Engine& engine = m_app.getEngine();
|
|
RenderInterface* ri = m_app.getRenderInterface();
|
|
for (FileInfo& info : m_file_infos) {
|
|
ri->unloadTexture(info.tex);
|
|
}
|
|
m_file_infos.clear();
|
|
|
|
Path::normalize(path, Span(m_dir.data));
|
|
if (push_history) pushDirHistory(m_dir);
|
|
int len = stringLength(m_dir);
|
|
if (len > 0 && (m_dir[len - 1] == '/' || m_dir[len - 1] == '\\')) {
|
|
m_dir.data[len - 1] = '\0';
|
|
}
|
|
|
|
FileSystem& fs = engine.getFileSystem();
|
|
os::FileIterator* iter = fs.createFileIterator(m_dir);
|
|
os::FileInfo info;
|
|
m_subdirs.clear();
|
|
while (os::getNextFile(iter, &info)) {
|
|
if (!info.is_directory) continue;
|
|
if (info.filename[0] != '.') m_subdirs.emplace(info.filename);
|
|
}
|
|
os::destroyFileIterator(iter);
|
|
|
|
AssetCompiler& compiler = m_app.getAssetCompiler();
|
|
char tmp[LUMIX_MAX_PATH];
|
|
makeLowercase(Span(tmp), m_dir.data);
|
|
const RuntimeHash dir_hash(equalStrings(".", tmp) ? "" : tmp);
|
|
auto& resources = compiler.lockResources();
|
|
if (m_filter[0]) {
|
|
for (const AssetCompiler::ResourceItem& res : resources) {
|
|
if (tmp[0] != '.' && tmp[1] != '\'' && !startsWithInsensitive(res.path.c_str(), tmp)) continue;
|
|
addTile(res.path);
|
|
}
|
|
}
|
|
else {
|
|
for (const AssetCompiler::ResourceItem& res : resources) {
|
|
if (res.dir_hash != dir_hash) continue;
|
|
addTile(res.path);
|
|
}
|
|
}
|
|
sortTiles();
|
|
compiler.unlockResources();
|
|
}
|
|
|
|
void sortTiles() {
|
|
qsort(m_file_infos.begin(), m_file_infos.size(), sizeof(m_file_infos[0]), [](const void* a, const void* b){
|
|
FileInfo* m = (FileInfo*)a;
|
|
FileInfo* n = (FileInfo*)b;
|
|
return strcmp(m->filepath.data, n->filepath.data);
|
|
});
|
|
}
|
|
|
|
void breadcrumbs()
|
|
{
|
|
const char* c = m_dir.data;
|
|
char tmp[LUMIX_MAX_PATH];
|
|
if (m_dir[0] != '.' || m_dir[1] != 0) {
|
|
if (ImGui::Button(".")) {
|
|
changeDir(".", true);
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::TextUnformatted("/");
|
|
ImGui::SameLine();
|
|
}
|
|
|
|
while (*c)
|
|
{
|
|
char* c_out = tmp;
|
|
while (*c && *c != '/')
|
|
{
|
|
*c_out = *c;
|
|
++c_out;
|
|
++c;
|
|
}
|
|
*c_out = '\0';
|
|
if (*c == '/') ++c;
|
|
if (ImGui::Button(tmp))
|
|
{
|
|
char new_dir[LUMIX_MAX_PATH];
|
|
copyNString(Span(new_dir), m_dir, int(c - m_dir.data));
|
|
changeDir(new_dir, true);
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::TextUnformatted("/");
|
|
ImGui::SameLine();
|
|
}
|
|
ImGui::NewLine();
|
|
}
|
|
|
|
|
|
void dirColumn()
|
|
{
|
|
ImVec2 size(maximum(120.f, m_left_column_width), 0);
|
|
ImGui::BeginChild("left_col", size);
|
|
ImGui::PushItemWidth(120);
|
|
bool b = false;
|
|
if ((m_dir[0] != '.' || m_dir[1] != 0) && ImGui::Selectable("..", &b))
|
|
{
|
|
char dir[LUMIX_MAX_PATH];
|
|
copyString(Span(dir), Path::getDir(m_dir));
|
|
changeDir(dir, true);
|
|
}
|
|
|
|
for (auto& subdir : m_subdirs)
|
|
{
|
|
if (ImGui::Selectable(subdir, &b))
|
|
{
|
|
StaticString<LUMIX_MAX_PATH> new_dir(m_dir, "/", subdir);
|
|
changeDir(new_dir, true);
|
|
}
|
|
}
|
|
|
|
ImGui::PopItemWidth();
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
|
|
int getThumbnailIndex(int i, int j, int columns) const
|
|
{
|
|
int idx = j * columns + i;
|
|
if (idx >= m_file_infos.size()) {
|
|
return -1;
|
|
}
|
|
return idx;
|
|
}
|
|
|
|
|
|
void createTile(FileInfo& tile, const char* out_path)
|
|
{
|
|
if (tile.create_called) return;
|
|
|
|
tile.create_called = true;
|
|
const AssetCompiler& compiler = m_app.getAssetCompiler();
|
|
for (Plugin* plugin : m_plugins) {
|
|
ResourceType type = compiler.getResourceType(tile.filepath);
|
|
if (plugin->createTile(tile.filepath, out_path, type)) break;
|
|
}
|
|
}
|
|
|
|
enum class TileState {
|
|
OK,
|
|
OUTDATED,
|
|
DELETED,
|
|
NOT_CREATED
|
|
};
|
|
|
|
static TileState getState(const FileInfo& info, FileSystem& fs) {
|
|
const Path path(".lumix/asset_tiles/", info.file_path_hash, ".lbc");
|
|
if (!fs.fileExists(info.filepath)) return TileState::DELETED;
|
|
if (!fs.fileExists(path)) return TileState::NOT_CREATED;
|
|
|
|
const Path compiled_path(".lumix/resources/", info.file_path_hash, ".res");
|
|
const u64 last_modified = fs.getLastModified(path);
|
|
if (last_modified < fs.getLastModified(info.filepath) || last_modified < fs.getLastModified(compiled_path)) {
|
|
return TileState::OUTDATED;
|
|
}
|
|
|
|
StaticString<LUMIX_MAX_PATH> meta_path(info.filepath, ".meta");
|
|
if (fs.getLastModified(meta_path) > last_modified) {
|
|
return TileState::OUTDATED;
|
|
}
|
|
|
|
return TileState::OK;
|
|
}
|
|
|
|
void thumbnail(FileInfo& tile, float size, bool selected)
|
|
{
|
|
ImGui::BeginGroup();
|
|
ImVec2 img_size(size, size);
|
|
RenderInterface* ri = m_app.getRenderInterface();
|
|
if (tile.tex)
|
|
{
|
|
if(ri->isValid(tile.tex)) {
|
|
ImGui::Image(*(void**)tile.tex, img_size);
|
|
}
|
|
else {
|
|
ImGui::Dummy(img_size);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ImGuiEx::Rect(img_size.x, img_size.y, 0xffffFFFF);
|
|
const Path compiled_asset_path(".lumix/resources/", tile.file_path_hash, ".res");
|
|
const Path path(".lumix/asset_tiles/", tile.file_path_hash, ".lbc");
|
|
FileSystem& fs = m_app.getEngine().getFileSystem();
|
|
switch (getState(tile, fs)) {
|
|
case TileState::OK:
|
|
tile.tex = ri->loadTexture(Path(path));
|
|
break;
|
|
case TileState::NOT_CREATED:
|
|
case TileState::OUTDATED:
|
|
createTile(tile, path);
|
|
break;
|
|
case TileState::DELETED:
|
|
break;
|
|
}
|
|
}
|
|
ImVec2 text_size = ImGui::CalcTextSize(tile.clamped_filename);
|
|
ImVec2 pos = ImGui::GetCursorPos();
|
|
pos.x += (size - text_size.x) * 0.5f;
|
|
ImGui::SetCursorPos(pos);
|
|
ImGui::Text("%s", tile.clamped_filename.data);
|
|
ImGui::EndGroup();
|
|
if (selected) {
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
const u32 color = ImGui::ColorConvertFloat4ToU32(ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]);
|
|
dl->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), color, 0, 0, 3.f);
|
|
}
|
|
}
|
|
|
|
void deleteTile(u32 idx) {
|
|
FileSystem& fs = m_app.getEngine().getFileSystem();
|
|
const Path res_path(".lumix/resources/", m_file_infos[idx].file_path_hash, ".res");
|
|
fs.deleteFile(res_path);
|
|
if (!fs.deleteFile(m_file_infos[idx].filepath)) {
|
|
logError("Failed to delete ", m_file_infos[idx].filepath);
|
|
}
|
|
}
|
|
|
|
void reloadTile(FilePathHash hash) override {
|
|
for (FileInfo& fi : m_file_infos) {
|
|
if (fi.file_path_hash == hash) {
|
|
m_app.getRenderInterface()->unloadTexture(fi.tex);
|
|
fi.tex = nullptr;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void recreateTiles() {
|
|
for (FileInfo& fi : m_file_infos) {
|
|
const Path path(".lumix/asset_tiles/", fi.file_path_hash, ".res");
|
|
createTile(fi, path);
|
|
}
|
|
}
|
|
|
|
void fileColumn()
|
|
{
|
|
ImGui::BeginChild("main_col");
|
|
|
|
float w = ImGui::GetContentRegionAvail().x;
|
|
int columns = m_show_thumbnails ? (int)w / int(TILE_SIZE * m_thumbnail_size) : 1;
|
|
columns = maximum(columns, 1);
|
|
int tile_count = m_file_infos.size();
|
|
int row_count = m_show_thumbnails ? (tile_count + columns - 1) / columns : tile_count;
|
|
|
|
auto callbacks = [this](FileInfo& tile, int idx) {
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tile.filepath.data);
|
|
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID))
|
|
{
|
|
ImGui::Text("%s", (const char*)tile.filepath);
|
|
ImGui::SetDragDropPayload("path", tile.filepath, stringLength(tile.filepath) + 1, ImGuiCond_Once);
|
|
ImGui::EndDragDropSource();
|
|
}
|
|
else if (ImGui::IsItemHovered())
|
|
{
|
|
if (ImGui::IsMouseReleased(0)) {
|
|
const bool additive = os::isKeyDown(os::Keycode::LSHIFT);
|
|
selectResource(Path(tile.filepath), true, additive);
|
|
}
|
|
else if(ImGui::IsMouseReleased(1)) {
|
|
m_context_resource = idx;
|
|
ImGui::OpenPopup("item_ctx");
|
|
}
|
|
}
|
|
};
|
|
|
|
ImGuiListClipper clipper;
|
|
clipper.Begin(row_count);
|
|
while (clipper.Step())
|
|
{
|
|
for (int j = clipper.DisplayStart; j < clipper.DisplayEnd; ++j)
|
|
{
|
|
if (m_show_thumbnails)
|
|
{
|
|
for (int i = 0; i < columns; ++i)
|
|
{
|
|
if (i > 0) ImGui::SameLine();
|
|
int idx = getThumbnailIndex(i, j, columns);
|
|
if (idx < 0) {
|
|
ImGui::NewLine();
|
|
break;
|
|
}
|
|
FileInfo& tile = m_file_infos[idx];
|
|
bool selected = m_selected_resources.find([&](Resource* res){ return res->getPath().getHash() == tile.file_path_hash; }) >= 0;
|
|
thumbnail(tile, m_thumbnail_size * TILE_SIZE, selected);
|
|
callbacks(tile, idx);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FileInfo& tile = m_file_infos[j];
|
|
bool b = m_selected_resources.find([&](Resource* res){ return res->getPath().getHash() == tile.file_path_hash; }) >= 0;
|
|
ImGui::Selectable(tile.filepath, b);
|
|
callbacks(tile, j);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool open_delete_popup = false;
|
|
FileSystem& fs = m_app.getEngine().getFileSystem();
|
|
static char tmp[LUMIX_MAX_PATH] = "";
|
|
auto common_popup = [&](){
|
|
const char* base_path = fs.getBasePath();
|
|
ImGui::Checkbox("Thumbnails", &m_show_thumbnails);
|
|
if (ImGui::Checkbox("Subresources", &m_show_subresources)) changeDir(m_dir, false);
|
|
if (ImGui::SliderFloat("Icon size", &m_thumbnail_size, 0.3f, 3.f, "%.2f", ImGuiSliderFlags_AlwaysClamp)) {
|
|
refreshLabels();
|
|
}
|
|
|
|
if (ImGui::MenuItem("View in explorer")) {
|
|
const Path dir_full_path(base_path, "/", m_dir);
|
|
os::openExplorer(dir_full_path);
|
|
}
|
|
if (ImGui::BeginMenu("Create directory")) {
|
|
ImGui::InputTextWithHint("##dirname", "New directory name", tmp, sizeof(tmp), ImGuiInputTextFlags_AutoSelectAll);
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Create")) {
|
|
const Path path(base_path, "/", m_dir, "/", tmp);
|
|
if (!os::makePath(path)) {
|
|
logError("Failed to create ", path);
|
|
}
|
|
changeDir(m_dir, false);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
for (Plugin* plugin : m_plugins) {
|
|
if (!plugin->canCreateResource()) continue;
|
|
if (ImGui::BeginMenu(plugin->getName())) {
|
|
bool input_entered = ImGui::InputTextWithHint("##name", "Name", tmp, sizeof(tmp), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll);
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Create") || input_entered) {
|
|
StaticString<LUMIX_MAX_PATH> path(m_dir, "/", tmp, ".", plugin->getDefaultExtension());
|
|
plugin->createResource(path);
|
|
m_wanted_resource = path;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
|
|
ImGui::EndMenu();
|
|
}
|
|
}
|
|
if (ImGui::MenuItem("Select all")) {
|
|
m_selected_resources.clear();
|
|
m_selected_resources.reserve(m_file_infos.size());
|
|
for (const FileInfo& fi : m_file_infos) {
|
|
selectResource(Path(fi.filepath), false, true);
|
|
}
|
|
}
|
|
if (ImGui::MenuItem("Recreate tiles")) {
|
|
recreateTiles();
|
|
}
|
|
};
|
|
if (ImGui::BeginPopup("item_ctx")) {
|
|
ImGui::Text("%s", m_file_infos[m_context_resource].clamped_filename.data);
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem(ICON_FA_EXTERNAL_LINK_ALT "Open externally")) {
|
|
openInExternalEditor(m_file_infos[m_context_resource].filepath);
|
|
}
|
|
if (ImGui::BeginMenu("Rename")) {
|
|
ImGui::InputTextWithHint("##New name", "New name", tmp, sizeof(tmp), ImGuiInputTextFlags_AutoSelectAll);
|
|
if (ImGui::Button("Rename", ImVec2(100, 0))) {
|
|
PathInfo fi(m_file_infos[m_context_resource].filepath);
|
|
StaticString<LUMIX_MAX_PATH> new_path(fi.m_dir, tmp, ".", fi.m_extension);
|
|
if (!fs.moveFile(m_file_infos[m_context_resource].filepath, new_path)) {
|
|
logError("Failed to rename ", m_file_infos[m_context_resource].filepath, " to ", new_path);
|
|
}
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
open_delete_popup = ImGui::MenuItem("Delete");
|
|
ImGui::Separator();
|
|
common_popup();
|
|
ImGui::EndPopup();
|
|
}
|
|
else if (ImGui::BeginPopupContextWindow("context")) {
|
|
common_popup();
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
if (open_delete_popup) ImGui::OpenPopup("Delete file");
|
|
|
|
if (ImGui::BeginPopupModal("Delete file", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::Text("Are you sure? This can not be undone.");
|
|
if (ImGui::Button("Yes, delete", ImVec2(100, 0))) {
|
|
deleteTile(m_context_resource);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine(ImGui::GetWindowWidth() - 100 - ImGui::GetStyle().WindowPadding.x);
|
|
if (ImGui::Button("Cancel", ImVec2(100, 0))) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
if (ImGui::BeginDragDropTarget()) {
|
|
if (auto* payload = ImGui::AcceptDragDropPayload("entity")) {
|
|
const EntityRef e = *(EntityRef*)payload->Data;
|
|
m_dropped_entity = e;
|
|
ImGui::OpenPopup("Save as prefab");
|
|
World* world = m_app.getWorldEditor().getWorld();
|
|
const ComponentType model_inst_type = reflection::getComponentType("model_instance");
|
|
IScene* scene = world->getScene(model_inst_type);
|
|
if (scene && world->hasComponent(e, model_inst_type)) {
|
|
Path source;
|
|
if (reflection::getPropertyValue(*scene, e, model_inst_type, "Source", source)) {
|
|
copyString(Span(m_prefab_name), Path::getBasename(source.c_str()));
|
|
}
|
|
}
|
|
}
|
|
ImGui::EndDragDropTarget();
|
|
}
|
|
ImGui::SetNextWindowSizeConstraints(ImVec2(200, 100), ImVec2(FLT_MAX, FLT_MAX));
|
|
if (ImGui::BeginPopupModal("Save as prefab")) {
|
|
ImGuiEx::Label("Name");
|
|
ImGui::InputText("##name", m_prefab_name, sizeof(m_prefab_name));
|
|
if (ImGui::Selectable(ICON_FA_SAVE "Save")) {
|
|
StaticString<LUMIX_MAX_PATH> path(m_dir, "/", m_prefab_name, ".fab");
|
|
m_app.getWorldEditor().getPrefabSystem().savePrefab((EntityRef)m_dropped_entity, Path(path));
|
|
m_dropped_entity = INVALID_ENTITY;
|
|
}
|
|
if (ImGui::Selectable(ICON_FA_TIMES "Cancel")) {
|
|
m_dropped_entity = INVALID_ENTITY;
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
|
|
void detailsGUI()
|
|
{
|
|
m_details_focused = false;
|
|
if (!m_is_open) return;
|
|
|
|
if (ImGui::Begin(INSPECTOR_NAME, &m_is_open, ImGuiWindowFlags_AlwaysVerticalScrollbar))
|
|
{
|
|
m_details_focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
|
|
m_has_focus = m_has_focus || m_details_focused;
|
|
|
|
ImVec2 pos = ImGui::GetCursorScreenPos();
|
|
if (m_history.size() > 1) {
|
|
if (ImGuiEx::BeginToolbar("asset_browser_toolbar", pos, ImVec2(0, 24)))
|
|
{
|
|
if (m_history_index > 0) m_back_action.toolbarButton(m_app.getBigIconFont());
|
|
if (m_history_index < m_history.size() - 1) m_forward_action.toolbarButton(m_app.getBigIconFont());
|
|
}
|
|
ImGuiEx::EndToolbar();
|
|
}
|
|
|
|
if (m_selected_resources.empty())
|
|
{
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
if (m_selected_resources.size() == 1) {
|
|
Resource* res = m_selected_resources[0];
|
|
const char* path = res->getPath().c_str();
|
|
ImGuiEx::Label("Selected resource");
|
|
ImGui::TextUnformatted(path);
|
|
ImGui::Separator();
|
|
|
|
ImGuiEx::Label("Status");
|
|
ImGui::TextUnformatted(res->isFailure() ? "failure" : (res->isReady() ? "Ready" : "Not ready"));
|
|
ImGuiEx::Label("Compiled size");
|
|
if (res->isReady()) {
|
|
ImGui::Text("%.2f KB", res->size() / 1024.f);
|
|
}
|
|
else {
|
|
ImGui::TextUnformatted("N/A");
|
|
}
|
|
const Span<const char> subres = getSubresource(m_selected_resources[0]->getPath().c_str());
|
|
if (*subres.end()) {
|
|
if (ImGui::Button("View parent")) {
|
|
selectResource(Path(getResourceFilePath(m_selected_resources[0]->getPath().c_str())), true, false);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
ImGui::Separator();
|
|
ImGuiEx::Label("Selected resource");
|
|
ImGui::TextUnformatted("multiple");
|
|
ImGui::Separator();
|
|
|
|
u32 ready = 0;
|
|
u32 failed = 0;
|
|
for (Resource* res : m_selected_resources) {
|
|
ready += res->isReady() ? 1 : 0;
|
|
failed += res->isFailure() ? 1 : 0;
|
|
}
|
|
|
|
ImGuiEx::Label("All");
|
|
ImGui::Text("%d", m_selected_resources.size());
|
|
ImGuiEx::Label("Ready");
|
|
ImGui::Text("%d", ready);
|
|
ImGuiEx::Label("Failed");
|
|
ImGui::Text("%d", failed);
|
|
}
|
|
|
|
const ResourceType type = m_selected_resources[0]->getType();
|
|
bool all_same_type = true;
|
|
for (Resource* res : m_selected_resources) {
|
|
all_same_type = all_same_type && res->getType() == type;
|
|
}
|
|
|
|
if (all_same_type) {
|
|
auto iter = m_plugins.find(type);
|
|
if (iter.isValid()) {
|
|
ImGui::Separator();
|
|
iter.value()->gui(m_selected_resources);
|
|
}
|
|
}
|
|
else {
|
|
ImGui::Text("Selected resources have different types.");
|
|
}
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
void redo() {
|
|
const ResourceType type = m_selected_resources[0]->getType();
|
|
bool all_same_type = true;
|
|
for (Resource* res : m_selected_resources) {
|
|
all_same_type = all_same_type && res->getType() == type;
|
|
}
|
|
|
|
if (all_same_type) {
|
|
auto iter = m_plugins.find(type);
|
|
if (iter.isValid()) {
|
|
iter.value()->redo();
|
|
}
|
|
}
|
|
}
|
|
|
|
void undo() {
|
|
const ResourceType type = m_selected_resources[0]->getType();
|
|
bool all_same_type = true;
|
|
for (Resource* res : m_selected_resources) {
|
|
all_same_type = all_same_type && res->getType() == type;
|
|
}
|
|
|
|
if (all_same_type) {
|
|
auto iter = m_plugins.find(type);
|
|
if (iter.isValid()) {
|
|
iter.value()->undo();
|
|
}
|
|
}
|
|
}
|
|
|
|
void refreshLabels() {
|
|
for (FileInfo& tile : m_file_infos) {
|
|
char filename[LUMIX_MAX_PATH];
|
|
Span<const char> subres = getSubresource(tile.filepath.data);
|
|
if (*subres.end()) {
|
|
copyNString(Span(filename), subres.begin(), subres.length());
|
|
catString(filename, ":");
|
|
catString(Span(filename), Path::getBasename(tile.filepath.data));
|
|
} else {
|
|
copyString(Span(filename), Path::getBasename(tile.filepath.data));
|
|
}
|
|
clampText(filename, int(TILE_SIZE * m_thumbnail_size));
|
|
|
|
tile.clamped_filename = filename;
|
|
}
|
|
}
|
|
|
|
const char* getName() const override { return "asset_browser"; }
|
|
|
|
void checkExtendedMouseButtons() {
|
|
if (!m_has_focus) return;
|
|
|
|
for (const os::Event e : m_app.getEvents()) {
|
|
if (e.type == os::Event::Type::MOUSE_BUTTON && !e.mouse_button.down) {
|
|
switch (e.mouse_button.button) {
|
|
case os::MouseButton::EXTENDED1: m_details_focused ? goBack() : goBackDir(); break;
|
|
case os::MouseButton::EXTENDED2: m_details_focused ? goForward() : goForwardDir(); break;
|
|
default: break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void onWindowGUI() override {
|
|
m_has_focus = false;
|
|
if (m_dir.data[0] == '\0') changeDir(".", true);
|
|
|
|
if (!m_wanted_resource.isEmpty()) {
|
|
selectResource(m_wanted_resource, true, false);
|
|
m_wanted_resource = "";
|
|
}
|
|
|
|
if(m_is_open) {
|
|
if (!ImGui::Begin(WINDOW_NAME, &m_is_open)) {
|
|
ImGui::End();
|
|
detailsGUI();
|
|
return;
|
|
}
|
|
m_has_focus = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) || m_has_focus;
|
|
|
|
ImGui::SetNextItemWidth(150);
|
|
if (ImGui::InputTextWithHint("##search", ICON_FA_SEARCH " Search", m_filter, sizeof(m_filter), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll)) changeDir(m_dir, false);
|
|
ImGui::SameLine();
|
|
if (ImGuiEx::IconButton(ICON_FA_TIMES, "Clear search")) {
|
|
m_filter[0] = '\0';
|
|
changeDir(m_dir, false);
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
breadcrumbs();
|
|
ImGui::Separator();
|
|
|
|
float content_w = ImGui::GetContentRegionAvail().x;
|
|
ImVec2 left_size(m_left_column_width, 0);
|
|
if (left_size.x < 10) left_size.x = 10;
|
|
if (left_size.x > content_w - 10) left_size.x = content_w - 10;
|
|
|
|
dirColumn();
|
|
|
|
ImGui::SameLine();
|
|
ImGuiEx::VSplitter("vsplit1", &left_size);
|
|
if (left_size.x >= 120) {
|
|
m_left_column_width = left_size.x;
|
|
}
|
|
ImGui::SameLine();
|
|
|
|
fileColumn();
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
detailsGUI();
|
|
checkExtendedMouseButtons();
|
|
}
|
|
|
|
void pushDirHistory(const char* path) {
|
|
if (m_dir_history_index + 1 == m_dir_history.size() && !m_dir_history.empty()) {
|
|
if (m_dir_history[m_dir_history_index] == path) return;
|
|
}
|
|
|
|
while (m_dir_history_index < m_dir_history.size() - 1) {
|
|
m_dir_history.pop();
|
|
}
|
|
++m_dir_history_index;
|
|
m_dir_history.push(Path(path));
|
|
|
|
if (m_dir_history.size() > 20) {
|
|
--m_dir_history_index;
|
|
m_dir_history.erase(0);
|
|
}
|
|
}
|
|
|
|
void selectResource(Resource* resource, bool record_history, bool additive)
|
|
{
|
|
if (record_history)
|
|
{
|
|
while (m_history_index < m_history.size() - 1)
|
|
{
|
|
m_history.pop();
|
|
}
|
|
m_history_index++;
|
|
m_history.push(resource->getPath());
|
|
|
|
if (m_history.size() > 20)
|
|
{
|
|
--m_history_index;
|
|
m_history.erase(0);
|
|
}
|
|
}
|
|
|
|
m_wanted_resource = "";
|
|
if(additive) {
|
|
if(m_selected_resources.indexOf(resource) >= 0) {
|
|
m_selected_resources.swapAndPopItem(resource);
|
|
}
|
|
else {
|
|
m_selected_resources.push(resource);
|
|
}
|
|
}
|
|
else {
|
|
unloadResources();
|
|
m_selected_resources.push(resource);
|
|
}
|
|
const char* path = resource->getPath().c_str();
|
|
ResourceLocator rl(Span(path, stringLength(path)));
|
|
char dir[LUMIX_MAX_PATH];
|
|
copyString(Span(dir), rl.dir);
|
|
changeDir(dir, false);
|
|
ASSERT(resource->getRefCount() > 0);
|
|
}
|
|
|
|
|
|
void removePlugin(Plugin& plugin) override
|
|
{
|
|
m_plugins.erase(plugin.getResourceType());
|
|
}
|
|
|
|
|
|
void addPlugin(Plugin& plugin) override
|
|
{
|
|
m_plugins.insert(plugin.getResourceType(), &plugin);
|
|
}
|
|
|
|
static void copyDir(const char* src, const char* dest, IAllocator& allocator)
|
|
{
|
|
PathInfo fi(src);
|
|
StaticString<LUMIX_MAX_PATH> dst_dir(dest, "/", fi.m_basename);
|
|
if (!os::makePath(dst_dir)) logError("Could not create ", dst_dir);
|
|
os::FileIterator* iter = os::createFileIterator(src, allocator);
|
|
|
|
os::FileInfo cfi;
|
|
while(os::getNextFile(iter, &cfi)) {
|
|
if (cfi.is_directory) {
|
|
if (cfi.filename[0] != '.') {
|
|
StaticString<LUMIX_MAX_PATH> tmp_src(src, "/", cfi.filename);
|
|
StaticString<LUMIX_MAX_PATH> tmp_dst(dest, "/", fi.m_basename);
|
|
copyDir(tmp_src, tmp_dst, allocator);
|
|
}
|
|
}
|
|
else {
|
|
StaticString<LUMIX_MAX_PATH> tmp_src(src, "/", cfi.filename);
|
|
StaticString<LUMIX_MAX_PATH> tmp_dst(dest, "/", fi.m_basename, "/", cfi.filename);
|
|
if(!os::copyFile(tmp_src, tmp_dst)) {
|
|
logError("Failed to copy ", tmp_src, " to ", tmp_dst);
|
|
}
|
|
}
|
|
}
|
|
os::destroyFileIterator(iter);
|
|
}
|
|
|
|
bool onDropFile(const char* path) override
|
|
{
|
|
FileSystem& fs = m_app.getEngine().getFileSystem();
|
|
if (os::dirExists(path)) {
|
|
const Path tmp(fs.getBasePath(), "/", m_dir, "/");
|
|
IAllocator& allocator = m_app.getAllocator();
|
|
copyDir(path, tmp, allocator);
|
|
}
|
|
PathInfo fi(path);
|
|
const Path dest(fs.getBasePath(), "/", m_dir, "/", fi.m_basename, ".", fi.m_extension);
|
|
return os::copyFile(path, dest);
|
|
}
|
|
|
|
static constexpr const char* WINDOW_NAME = ICON_FA_IMAGES "Assets##assets";
|
|
static constexpr const char* INSPECTOR_NAME = ICON_FA_IMAGE "Asset inspector##asset_inspector";
|
|
|
|
void selectResource(const Path& path, bool record_history, bool additive) override
|
|
{
|
|
ImGui::SetWindowFocus(INSPECTOR_NAME);
|
|
ImGui::SetWindowFocus(WINDOW_NAME);
|
|
m_is_open = true;
|
|
auto& manager = m_app.getEngine().getResourceManager();
|
|
const AssetCompiler& compiler = m_app.getAssetCompiler();
|
|
const ResourceType type = compiler.getResourceType(path.c_str());
|
|
Resource* res = manager.load(type, path);
|
|
if (res) selectResource(res, record_history, additive);
|
|
}
|
|
|
|
static StaticString<LUMIX_MAX_PATH> getImGuiLabelID(const ResourceLocator& rl, bool hash_id) {
|
|
StaticString<LUMIX_MAX_PATH> res("");
|
|
if (rl.full.length() > 0) {
|
|
res.append(rl.subresource, (rl.subresource.length() > 0 ? ":" : ""), rl.basename, ".", rl.ext);
|
|
}
|
|
if (hash_id) {
|
|
res.append("##h", RuntimeHash(rl.full.m_begin, rl.full.length()).getHashValue());
|
|
}
|
|
return res;
|
|
}
|
|
|
|
bool resourceInput(const char* str_id, Span<char> buf, ResourceType type, float width) override
|
|
{
|
|
ImGui::PushID(str_id);
|
|
|
|
const Span span(buf.m_begin, stringLength(buf.m_begin));
|
|
const ResourceLocator rl(span);
|
|
|
|
bool popup_opened = false;
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, ImGui::GetStyle().ItemSpacing.y));
|
|
if (span.length() == 0) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]);
|
|
if (ImGui::Button("No resource (click to set)", ImVec2(width, 0))) {
|
|
ImGui::OpenPopup("popup");
|
|
popup_opened = true;
|
|
}
|
|
}
|
|
else {
|
|
float w = ImGui::CalcTextSize(ICON_FA_BULLSEYE ICON_FA_TRASH).x;
|
|
if (ImGui::Button(getImGuiLabelID(rl, false).data, ImVec2(width < 0 ? -w : width - w, 0))) {
|
|
ImGui::OpenPopup("popup");
|
|
popup_opened = true;
|
|
}
|
|
}
|
|
if (span.length() == 0) {
|
|
ImGui::PopStyleColor();
|
|
}
|
|
if (span.length() > 0 && ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("%s", buf.m_begin);
|
|
}
|
|
|
|
if (ImGui::BeginDragDropTarget()) {
|
|
if (auto* payload = ImGui::AcceptDragDropPayload("path")) {
|
|
char ext[10];
|
|
const char* path = (const char*)payload->Data;
|
|
Span<const char> subres = getSubresource(path);
|
|
copyString(Span(ext), Path::getExtension(subres));
|
|
const AssetCompiler& compiler = m_app.getAssetCompiler();
|
|
if (compiler.acceptExtension(ext, type)) {
|
|
copyString(buf, path);
|
|
ImGui::EndDragDropTarget();
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopID();
|
|
return true;
|
|
}
|
|
}
|
|
ImGui::EndDragDropTarget();
|
|
}
|
|
if (span.length() > 0) {
|
|
ImGui::SameLine();
|
|
if (ImGuiEx::IconButton(ICON_FA_BULLSEYE, "Go to")) {
|
|
m_wanted_resource = buf.begin();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGuiEx::IconButton(ICON_FA_TRASH, "Clear")) {
|
|
copyString(buf, "");
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopID();
|
|
return true;
|
|
}
|
|
}
|
|
ImGui::PopStyleVar();
|
|
|
|
if (ImGuiEx::BeginResizablePopup("popup", ImVec2(200, 300))) {
|
|
static FilePathHash selected_path_hash;
|
|
if (popup_opened) ImGui::SetKeyboardFocusHere();
|
|
if (resourceList(buf, selected_path_hash, type, true, true)) {
|
|
ImGui::EndPopup();
|
|
ImGui::PopID();
|
|
return true;
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopID();
|
|
return false;
|
|
}
|
|
|
|
|
|
void saveResource(Resource& resource, OutputMemoryStream& stream) override
|
|
{
|
|
FileSystem& fs = m_app.getEngine().getFileSystem();
|
|
// use temporary because otherwise the resource is reloaded during saving
|
|
StaticString<LUMIX_MAX_PATH> tmp_path(resource.getPath().c_str(), ".tmp");
|
|
|
|
if (!fs.saveContentSync(Path(tmp_path), stream)) {
|
|
logError("Could not save file ", resource.getPath());
|
|
return;
|
|
}
|
|
|
|
Engine& engine = m_app.getEngine();
|
|
const char* base_path = engine.getFileSystem().getBasePath();
|
|
StaticString<LUMIX_MAX_PATH> src_full_path(base_path, tmp_path);
|
|
StaticString<LUMIX_MAX_PATH> dest_full_path(base_path, resource.getPath().c_str());
|
|
|
|
os::deleteFile(dest_full_path);
|
|
|
|
if (!os::moveFile(src_full_path, dest_full_path))
|
|
{
|
|
logError("Could not save file ", resource.getPath());
|
|
}
|
|
}
|
|
|
|
void tile(const Path& path, bool selected) override {
|
|
i32 idx = m_immediate_tiles.find([&path](const FileInfo& fi){
|
|
return fi.file_path_hash == path.getHash();
|
|
});
|
|
if (idx < 0) {
|
|
FileInfo& fi = m_immediate_tiles.emplace();
|
|
fi.file_path_hash = path.getHash();
|
|
fi.filepath = path.c_str();
|
|
|
|
char filename[LUMIX_MAX_PATH];
|
|
Span<const char> subres = getSubresource(path.c_str());
|
|
if (*subres.end()) {
|
|
copyNString(Span(filename), subres.begin(), subres.length());
|
|
catString(filename, ":");
|
|
catString(Span(filename), Path::getBasename(path.c_str()));
|
|
}
|
|
else {
|
|
copyString(Span(filename), Path::getBasename(path.c_str()));
|
|
}
|
|
clampText(filename, 50);
|
|
fi.clamped_filename = filename;
|
|
fi.create_called = false;
|
|
idx = m_immediate_tiles.size() - 1;
|
|
}
|
|
|
|
m_immediate_tiles[idx].gc_counter = 2;
|
|
thumbnail(m_immediate_tiles[idx], 50.f, selected);
|
|
}
|
|
|
|
bool resourceList(Span<char> buf, FilePathHash& selected_path_hash, ResourceType type, bool can_create_new, bool enter_submit) const override {
|
|
auto iter = m_plugins.find(type);
|
|
if (!iter.isValid()) return false;
|
|
|
|
Plugin* plugin = iter.value();
|
|
|
|
static bool show_new_fs = false;
|
|
if (can_create_new && plugin->canCreateResource() && ImGui::Selectable("New", false, ImGuiSelectableFlags_DontClosePopups)) {
|
|
show_new_fs = true;
|
|
}
|
|
|
|
FileSelector& file_selector = m_app.getFileSelector();
|
|
if (file_selector.gui("Save As", &show_new_fs, plugin->getDefaultExtension(), true)) {
|
|
if (!plugin->createResource(file_selector.getPath())) {
|
|
logError("Failed to create ", file_selector.getPath());
|
|
return false;
|
|
}
|
|
copyString(buf, file_selector.getPath());
|
|
return true;
|
|
}
|
|
|
|
static char filter[128] = "";
|
|
ImGuiEx::filter("Filter", filter, sizeof(filter), 200);
|
|
|
|
ImGui::BeginChild("Resources", ImVec2(0, 200), false, ImGuiWindowFlags_HorizontalScrollbar);
|
|
AssetCompiler& compiler = m_app.getAssetCompiler();
|
|
|
|
const auto& resources = compiler.lockResources();
|
|
Path selected_path;
|
|
for (const auto& res : resources) {
|
|
if(res.type != type) continue;
|
|
if (filter[0] != '\0' && stristr(res.path.c_str(), filter) == nullptr) continue;
|
|
|
|
const bool selected = selected_path_hash == res.path.getHash();
|
|
if(selected) selected_path = res.path;
|
|
const Span span(res.path.c_str(), res.path.length());
|
|
const ResourceLocator rl(span);
|
|
StaticString<LUMIX_MAX_PATH> label = getImGuiLabelID(rl, true);
|
|
const bool is_enter_submit = (enter_submit && ImGui::IsKeyPressed(ImGuiKey_Enter));
|
|
if (is_enter_submit || ImGui::Selectable(label, selected, ImGuiSelectableFlags_AllowDoubleClick)) {
|
|
selected_path_hash = res.path.getHash();
|
|
|
|
if (selected || ImGui::IsMouseDoubleClicked(0) || is_enter_submit) {
|
|
copyString(buf, res.path.c_str());
|
|
ImGui::CloseCurrentPopup();
|
|
ImGui::EndChild();
|
|
compiler.unlockResources();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
ImGui::Separator();
|
|
if (!selected_path.isEmpty()) {
|
|
ImGui::TextWrapped("%s", selected_path.c_str());
|
|
}
|
|
compiler.unlockResources();
|
|
return false;
|
|
}
|
|
|
|
|
|
void openInExternalEditor(Resource* resource) const override
|
|
{
|
|
openInExternalEditor(resource->getPath().c_str());
|
|
}
|
|
|
|
|
|
void openInExternalEditor(const char* path) const override
|
|
{
|
|
const char* base_path = m_app.getEngine().getFileSystem().getBasePath();
|
|
StaticString<LUMIX_MAX_PATH> full_path(base_path, path);
|
|
const os::ExecuteOpenResult res = os::shellExecuteOpen(full_path);
|
|
if (res == os::ExecuteOpenResult::NO_ASSOCIATION) {
|
|
logError(full_path, " is not associated with any app.");
|
|
}
|
|
else if (res == os::ExecuteOpenResult::OTHER_ERROR) {
|
|
logError("Failed to open ", full_path, " in exeternal editor.");
|
|
}
|
|
}
|
|
|
|
void goBackDir() {
|
|
if (m_dir_history_index < 1) return;
|
|
m_dir_history_index = maximum(0, m_dir_history_index - 1);
|
|
changeDir(m_dir_history[m_dir_history_index].c_str(), false);
|
|
}
|
|
|
|
void goForwardDir() {
|
|
m_dir_history_index = minimum(m_dir_history_index + 1, m_dir_history.size() - 1);
|
|
changeDir(m_dir_history[m_dir_history_index].c_str(), false);
|
|
}
|
|
|
|
void goBack() {
|
|
if (m_history_index < 1) return;
|
|
m_history_index = maximum(0, m_history_index - 1);
|
|
selectResource(m_history[m_history_index], false, false);
|
|
}
|
|
|
|
void goForward() {
|
|
m_history_index = minimum(m_history_index + 1, m_history.size() - 1);
|
|
selectResource(m_history[m_history_index], false, false);
|
|
}
|
|
|
|
bool isOpen() const { return m_is_open; }
|
|
void toggleUI() { m_is_open = !m_is_open; }
|
|
|
|
void onSettingsLoaded() override { m_is_open = m_app.getSettings().m_is_asset_browser_open; }
|
|
void onBeforeSettingsSaved() override { m_app.getSettings().m_is_asset_browser_open = m_is_open; }
|
|
|
|
bool copyTile(const char* from, const char* to) override {
|
|
OutputMemoryStream img(m_app.getAllocator());
|
|
if (!m_app.getEngine().getFileSystem().getContentSync(Path(from), img)) return false;
|
|
|
|
os::OutputFile file;
|
|
if (!m_app.getEngine().getFileSystem().open(to, file)) return false;
|
|
Span<const char> ext = Path::getExtension(Span(from, stringLength(from)));
|
|
if (ext.length() != 3) {
|
|
file.close();
|
|
return false;
|
|
}
|
|
char ext_tmp[4];
|
|
makeLowercase(Span(ext_tmp), ext.begin());
|
|
(void)file.write(ext_tmp, 3);
|
|
(void)file.write(u32(0));
|
|
(void)file.write(img.data(), img.size());
|
|
file.close();
|
|
return !file.isError();
|
|
}
|
|
|
|
bool m_is_open;
|
|
float m_left_column_width = 120;
|
|
StudioApp& m_app;
|
|
StaticString<LUMIX_MAX_PATH> m_dir;
|
|
Array<StaticString<LUMIX_MAX_PATH> > m_subdirs;
|
|
Array<FileInfo> m_file_infos;
|
|
Array<ImmediateTile> m_immediate_tiles;
|
|
|
|
Array<Path> m_history;
|
|
i32 m_history_index = -1;
|
|
|
|
Array<Path> m_dir_history;
|
|
i32 m_dir_history_index = -1;
|
|
|
|
EntityPtr m_dropped_entity = INVALID_ENTITY;
|
|
char m_prefab_name[LUMIX_MAX_PATH] = "";
|
|
HashMap<ResourceType, Plugin*> m_plugins;
|
|
Array<Resource*> m_selected_resources;
|
|
int m_context_resource;
|
|
char m_filter[128];
|
|
Path m_wanted_resource;
|
|
bool m_show_thumbnails;
|
|
bool m_show_subresources;
|
|
bool m_has_focus = false;
|
|
bool m_details_focused = false;
|
|
float m_thumbnail_size = 1.f;
|
|
Action m_toggle_ui;
|
|
Action m_back_action;
|
|
Action m_forward_action;
|
|
Action m_undo_action;
|
|
Action m_redo_action;
|
|
};
|
|
|
|
UniquePtr<AssetBrowser> AssetBrowser::create(StudioApp& app) {
|
|
return UniquePtr<AssetBrowserImpl>::create(app.getAllocator(), app);
|
|
}
|
|
|
|
} // namespace Lumix
|