3573 lines
108 KiB
C++
3573 lines
108 KiB
C++
#include <imgui/imgui.h>
|
|
#include <imgui/imgui_internal.h>
|
|
#include <imgui/imgui_freetype.h>
|
|
|
|
#include "audio/audio_module.h"
|
|
#include "editor/asset_browser.h"
|
|
#include "editor/asset_compiler.h"
|
|
#include "editor/entity_folders.h"
|
|
#include "editor/file_system_watcher.h"
|
|
#include "editor/gizmo.h"
|
|
#include "editor/prefab_system.h"
|
|
#include "editor/render_interface.h"
|
|
#include "editor/spline_editor.h"
|
|
#include "editor/world_editor.h"
|
|
#include "engine/allocators.h"
|
|
#include "engine/associative_array.h"
|
|
#include "engine/atomic.h"
|
|
#include "engine/command_line_parser.h"
|
|
#include "engine/debug.h"
|
|
#include "engine/engine.h"
|
|
#include "engine/file_system.h"
|
|
#include "engine/geometry.h"
|
|
#include "engine/hash.h"
|
|
#include "engine/input_system.h"
|
|
#include "engine/job_system.h"
|
|
#include "engine/log.h"
|
|
#include "engine/lua_wrapper.h"
|
|
#include "engine/os.h"
|
|
#include "engine/path.h"
|
|
#include "engine/profiler.h"
|
|
#include "engine/reflection.h"
|
|
#include "engine/resource_manager.h"
|
|
#include "engine/world.h"
|
|
#include "log_ui.h"
|
|
#include "profiler_ui.h"
|
|
#include "property_grid.h"
|
|
#include "settings.h"
|
|
#include "studio_app.h"
|
|
#include "utils.h"
|
|
|
|
#ifdef _WIN32
|
|
#include "engine/win/simple_win.h"
|
|
#endif
|
|
|
|
namespace Lumix
|
|
{
|
|
|
|
#define LUMIX_EDITOR_PLUGINS_DECLS
|
|
#include "engine/plugins.inl"
|
|
#undef LUMIX_EDITOR_PLUGINS_DECLS
|
|
|
|
struct TarHeader {
|
|
char name[100];
|
|
char mode[8];
|
|
char uid[8];
|
|
char gid[8];
|
|
char size[12];
|
|
char mtime[12];
|
|
char chksum[8];
|
|
char typeflag;
|
|
char linkname[100];
|
|
char magic[6];
|
|
char version[2];
|
|
char uname[32];
|
|
char gname[32];
|
|
char devmajor[8];
|
|
char devminor[8];
|
|
char prefix[155];
|
|
};
|
|
|
|
|
|
struct StudioAppImpl final : StudioApp
|
|
{
|
|
StudioAppImpl()
|
|
: m_is_entity_list_open(true)
|
|
, m_finished(false)
|
|
, m_deferred_game_mode_exit(false)
|
|
, m_actions(m_allocator)
|
|
, m_owned_actions(m_allocator)
|
|
, m_window_actions(m_allocator)
|
|
, m_tools_actions(m_allocator)
|
|
, m_is_welcome_screen_open(true)
|
|
, m_is_export_game_dialog_open(false)
|
|
, m_settings(*this)
|
|
, m_gui_plugins(m_allocator)
|
|
, m_mouse_plugins(m_allocator)
|
|
, m_plugins(m_allocator)
|
|
, m_add_cmp_plugins(m_allocator)
|
|
, m_component_labels(m_allocator)
|
|
, m_component_icons(m_allocator)
|
|
, m_exit_code(0)
|
|
, m_events(m_allocator)
|
|
, m_windows(m_allocator)
|
|
, m_deferred_destroy_windows(m_allocator)
|
|
, m_file_selector(*this)
|
|
, m_dir_selector(*this)
|
|
, m_debug_allocator(m_main_allocator)
|
|
, m_imgui_allocator(m_debug_allocator, "imgui")
|
|
, m_allocator(m_debug_allocator, "studio")
|
|
{
|
|
PROFILE_FUNCTION();
|
|
u32 cpus_count = minimum(os::getCPUsCount(), 64);
|
|
u32 workers;
|
|
if (workersCountOption(workers)) {
|
|
cpus_count = workers;
|
|
}
|
|
if (!jobs::init(cpus_count, m_allocator)) {
|
|
logError("Failed to initialize job system.");
|
|
}
|
|
|
|
memset(m_imgui_key_map, 0, sizeof(m_imgui_key_map));
|
|
m_imgui_key_map[(int)os::Keycode::CTRL] = ImGuiMod_Ctrl;
|
|
m_imgui_key_map[(int)os::Keycode::ALT] = ImGuiMod_Alt;
|
|
m_imgui_key_map[(int)os::Keycode::SHIFT] = ImGuiMod_Shift;
|
|
m_imgui_key_map[(int)os::Keycode::LSHIFT] = ImGuiKey_LeftShift;
|
|
m_imgui_key_map[(int)os::Keycode::RSHIFT] = ImGuiKey_RightShift;
|
|
m_imgui_key_map[(int)os::Keycode::SPACE] = ImGuiKey_Space;
|
|
m_imgui_key_map[(int)os::Keycode::TAB] = ImGuiKey_Tab;
|
|
m_imgui_key_map[(int)os::Keycode::LEFT] = ImGuiKey_LeftArrow;
|
|
m_imgui_key_map[(int)os::Keycode::RIGHT] = ImGuiKey_RightArrow;
|
|
m_imgui_key_map[(int)os::Keycode::UP] = ImGuiKey_UpArrow;
|
|
m_imgui_key_map[(int)os::Keycode::DOWN] = ImGuiKey_DownArrow;
|
|
m_imgui_key_map[(int)os::Keycode::PAGEUP] = ImGuiKey_PageUp;
|
|
m_imgui_key_map[(int)os::Keycode::PAGEDOWN] = ImGuiKey_PageDown;
|
|
m_imgui_key_map[(int)os::Keycode::HOME] = ImGuiKey_Home;
|
|
m_imgui_key_map[(int)os::Keycode::END] = ImGuiKey_End;
|
|
m_imgui_key_map[(int)os::Keycode::DEL] = ImGuiKey_Delete;
|
|
m_imgui_key_map[(int)os::Keycode::BACKSPACE] = ImGuiKey_Backspace;
|
|
m_imgui_key_map[(int)os::Keycode::F3] = ImGuiKey_F3;
|
|
m_imgui_key_map[(int)os::Keycode::F11] = ImGuiKey_F11;
|
|
m_imgui_key_map[(int)os::Keycode::RETURN] = ImGuiKey_Enter;
|
|
m_imgui_key_map[(int)os::Keycode::ESCAPE] = ImGuiKey_Escape;
|
|
m_imgui_key_map[(int)os::Keycode::A] = ImGuiKey_A;
|
|
m_imgui_key_map[(int)os::Keycode::C] = ImGuiKey_C;
|
|
m_imgui_key_map[(int)os::Keycode::D] = ImGuiKey_D;
|
|
m_imgui_key_map[(int)os::Keycode::F] = ImGuiKey_F;
|
|
m_imgui_key_map[(int)os::Keycode::V] = ImGuiKey_V;
|
|
m_imgui_key_map[(int)os::Keycode::X] = ImGuiKey_X;
|
|
m_imgui_key_map[(int)os::Keycode::Y] = ImGuiKey_Y;
|
|
m_imgui_key_map[(int)os::Keycode::Z] = ImGuiKey_Z;
|
|
}
|
|
|
|
void onEvent(const os::Event& event)
|
|
{
|
|
const bool handle_input = isFocused();
|
|
m_events.push(event);
|
|
switch (event.type) {
|
|
case os::Event::Type::MOUSE_MOVE: break;
|
|
case os::Event::Type::FOCUS: {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.AddFocusEvent(isFocused());
|
|
break;
|
|
}
|
|
case os::Event::Type::MOUSE_BUTTON: {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
if (handle_input || !event.mouse_button.down) {
|
|
io.AddMouseButtonEvent((int)event.mouse_button.button, event.mouse_button.down);
|
|
}
|
|
break;
|
|
}
|
|
case os::Event::Type::MOUSE_WHEEL:
|
|
if (handle_input) {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.AddMouseWheelEvent(0, event.mouse_wheel.amount);
|
|
}
|
|
break;
|
|
case os::Event::Type::WINDOW_SIZE:
|
|
if (ImGui::GetCurrentContext()) {
|
|
ImGuiViewport* vp = ImGui::FindViewportByPlatformHandle(event.window);
|
|
if (vp) vp->PlatformRequestResize = true;
|
|
}
|
|
break;
|
|
case os::Event::Type::WINDOW_MOVE:
|
|
if (ImGui::GetCurrentContext()) {
|
|
ImGuiViewport* vp = ImGui::FindViewportByPlatformHandle(event.window);
|
|
if (vp) vp->PlatformRequestMove = true;
|
|
}
|
|
break;
|
|
case os::Event::Type::WINDOW_CLOSE: {
|
|
ImGuiViewport* vp = ImGui::FindViewportByPlatformHandle(event.window);
|
|
if (vp) vp->PlatformRequestClose = true;
|
|
if (event.window == m_main_window) exit();
|
|
break;
|
|
}
|
|
case os::Event::Type::QUIT:
|
|
exit();
|
|
break;
|
|
case os::Event::Type::CHAR:
|
|
if (handle_input) {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
char tmp[5] = {};
|
|
memcpy(tmp, &event.text_input.utf8, sizeof(event.text_input.utf8));
|
|
io.AddInputCharactersUTF8(tmp);
|
|
}
|
|
break;
|
|
case os::Event::Type::KEY:
|
|
if (handle_input || !event.key.down) {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
ImGuiKey key = m_imgui_key_map[(int)event.key.keycode];
|
|
if (key != ImGuiKey_None) io.AddKeyEvent(key, event.key.down);
|
|
|
|
if (event.key.down && event.key.keycode == os::Keycode::F2) {
|
|
m_is_f2_pressed = true;
|
|
}
|
|
checkShortcuts();
|
|
}
|
|
break;
|
|
case os::Event::Type::DROP_FILE:
|
|
for(int i = 0, c = os::getDropFileCount(event); i < c; ++i) {
|
|
char tmp[MAX_PATH];
|
|
if (os::getDropFile(event, i, Span(tmp))) {
|
|
for (GUIPlugin* plugin : m_gui_plugins) {
|
|
if (plugin->onDropFile(tmp)) break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool isFocused() const {
|
|
const os::WindowHandle focused = os::getFocused();
|
|
const int idx = m_windows.find([focused](os::WindowHandle w){ return w == focused; });
|
|
return idx >= 0;
|
|
}
|
|
|
|
void onShutdown() {
|
|
while (m_engine->getFileSystem().hasWork()) {
|
|
m_engine->getFileSystem().processCallbacks();
|
|
}
|
|
}
|
|
|
|
void onIdle() {
|
|
update();
|
|
|
|
if (!isFocused()) ++m_frames_since_focused;
|
|
else m_frames_since_focused = 0;
|
|
|
|
if (m_settings.m_sleep_when_inactive && m_frames_since_focused > 10) {
|
|
const float frame_time = m_inactive_fps_timer.tick();
|
|
const float wanted_fps = 5.0f;
|
|
|
|
if (frame_time < 1 / wanted_fps) {
|
|
PROFILE_BLOCK("sleep");
|
|
os::sleep(u32(1000 / wanted_fps - frame_time * 1000));
|
|
}
|
|
m_inactive_fps_timer.tick();
|
|
}
|
|
|
|
profiler::frame();
|
|
m_events.clear();
|
|
m_is_f2_pressed = false;
|
|
}
|
|
|
|
bool profileStart() {
|
|
char cmd_line[2048];
|
|
os::getCommandLine(Span(cmd_line));
|
|
|
|
CommandLineParser parser(cmd_line);
|
|
while (parser.next()) {
|
|
if (parser.currentEquals("-profile_start")) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void run() override {
|
|
profiler::setThreadName("Main thread");
|
|
Semaphore semaphore(0, 1);
|
|
struct Data {
|
|
StudioAppImpl* that;
|
|
Semaphore* semaphore;
|
|
} data = {this, &semaphore};
|
|
jobs::runLambda([&data]() {
|
|
data.that->onInit();
|
|
if (data.that->profileStart()) {
|
|
profiler::pause(true);
|
|
}
|
|
while (!data.that->m_finished) {
|
|
os::Event e;
|
|
while(os::getEvent(e)) {
|
|
data.that->onEvent(e);
|
|
}
|
|
data.that->onIdle();
|
|
}
|
|
data.that->onShutdown();
|
|
data.semaphore->signal();
|
|
}, nullptr, 0);
|
|
PROFILE_BLOCK("sleeping");
|
|
semaphore.wait();
|
|
}
|
|
|
|
|
|
static void* imguiAlloc(size_t size, void* user_data) {
|
|
StudioAppImpl* app = (StudioAppImpl*)user_data;
|
|
return app->m_imgui_allocator.allocate(size, 8);
|
|
}
|
|
|
|
|
|
static void imguiFree(void* ptr, void* user_data) {
|
|
StudioAppImpl* app = (StudioAppImpl*)user_data;
|
|
return app->m_imgui_allocator.deallocate(ptr);
|
|
}
|
|
|
|
void onEntitySelectionChanged() {
|
|
m_entity_selection_changed = true;
|
|
}
|
|
|
|
static os::HitTestResult childHitTestCallback(void* user_data, os::WindowHandle window, os::Point mp) {
|
|
#if 1
|
|
// let imgui handle size of secondary windows
|
|
// otherwise it has issues with docking
|
|
return os::HitTestResult::CLIENT;
|
|
#else
|
|
StudioAppImpl* studio = (StudioAppImpl*)user_data;
|
|
if (mp.y < os::getWindowScreenRect(window).top + 20) return os::HitTestResult::CAPTION;
|
|
if (ImGui::IsAnyItemHovered()) return os::HitTestResult::CLIENT;
|
|
return os::HitTestResult::NONE;
|
|
#endif
|
|
}
|
|
|
|
static os::HitTestResult hitTestCallback(void* user_data, os::WindowHandle window, os::Point mp) {
|
|
StudioAppImpl* studio = (StudioAppImpl*)user_data;
|
|
if (studio->m_is_caption_hovered) return os::HitTestResult::CAPTION;
|
|
if (ImGui::IsAnyItemHovered()) return os::HitTestResult::CLIENT;
|
|
return os::HitTestResult::NONE;
|
|
}
|
|
|
|
void onInit()
|
|
{
|
|
PROFILE_FUNCTION();
|
|
|
|
os::Timer init_timer;
|
|
m_add_cmp_root.label[0] = '\0';
|
|
|
|
char saved_data_dir[MAX_PATH] = {};
|
|
os::InputFile cfg_file;
|
|
if (cfg_file.open(".lumixuser")) {
|
|
if (!cfg_file.read(saved_data_dir, minimum(lengthOf(saved_data_dir), (int)cfg_file.size()))) {
|
|
logError("Failed to read .lumixuser");
|
|
}
|
|
cfg_file.close();
|
|
}
|
|
|
|
char current_dir[MAX_PATH] = "";
|
|
os::getCurrentDirectory(Span(current_dir));
|
|
|
|
char data_dir[MAX_PATH] = "";
|
|
checkDataDirCommandLine(data_dir, lengthOf(data_dir));
|
|
|
|
Engine::InitArgs init_data = {};
|
|
init_data.init_window_args.handle_file_drops = true;
|
|
init_data.init_window_args.name = "Lumix Studio";
|
|
init_data.working_dir = data_dir[0] ? data_dir : (saved_data_dir[0] ? saved_data_dir : current_dir);
|
|
init_data.init_window_args.user_data = this;
|
|
init_data.init_window_args.hit_test_callback = &StudioAppImpl::hitTestCallback;
|
|
init_data.init_window_args.flags = os::InitWindowArgs::NO_DECORATION;
|
|
const char* plugins[] = {
|
|
#define LUMIX_PLUGINS_STRINGS
|
|
#include "engine/plugins.inl"
|
|
#undef LUMIX_PLUGINS_STRINGS
|
|
};
|
|
init_data.plugins = Span(plugins, plugins + lengthOf(plugins) - 1);
|
|
init_data.init_window_args.icon = "editor/logo.ico";
|
|
m_engine = Engine::create(static_cast<Engine::InitArgs&&>(init_data), m_allocator);
|
|
m_main_window = m_engine->getWindowHandle();
|
|
m_windows.push(m_main_window);
|
|
|
|
beginInitIMGUI();
|
|
m_engine->init();
|
|
jobs::wait(&m_init_imgui_signal);
|
|
|
|
logInfo("Current directory: ", current_dir);
|
|
|
|
registerLuaAPI();
|
|
extractBundled();
|
|
|
|
m_asset_compiler = AssetCompiler::create(*this);
|
|
m_editor = WorldEditor::create(*m_engine, m_allocator);
|
|
m_editor->entitySelectionChanged().bind<&StudioAppImpl::onEntitySelectionChanged>(this);
|
|
loadUserPlugins();
|
|
addActions();
|
|
|
|
m_asset_browser = AssetBrowser::create(*this);
|
|
m_property_grid.create(*this);
|
|
m_profiler_ui = createProfilerUI(*this);
|
|
m_log_ui.create(*this, m_allocator);
|
|
|
|
// TODO refactor so we don't need to call loadSettings twice (once in beginInitIMGUI)
|
|
initPlugins(); // needs initialized imgui
|
|
loadSettings(); // needs plugins
|
|
|
|
loadWorldFromCommandLine();
|
|
|
|
m_asset_compiler->onInitFinished();
|
|
m_asset_browser->onInitFinished();
|
|
|
|
checkScriptCommandLine();
|
|
|
|
logInfo("Init took ", init_timer.getTimeSinceStart(), " s");
|
|
#ifdef _WIN32
|
|
logInfo(os::getTimeSinceProcessStart(), " s since process started");
|
|
#endif
|
|
|
|
loadLogo();
|
|
}
|
|
|
|
void loadLogo() {
|
|
if (!m_render_interface) return;
|
|
m_logo = m_render_interface->loadTexture(Path("editor/logo.png"));
|
|
}
|
|
|
|
~StudioAppImpl()
|
|
{
|
|
removeAction(&m_show_all_actions_action);
|
|
removePlugin(*m_asset_browser.get());
|
|
removePlugin(*m_log_ui.get());
|
|
removePlugin(*m_property_grid.get());
|
|
removePlugin(*m_profiler_ui.get());
|
|
|
|
m_asset_browser->releaseResources();
|
|
m_watched_plugin.watcher.reset();
|
|
|
|
saveSettings();
|
|
|
|
while (m_engine->getFileSystem().hasWork()) {
|
|
m_engine->getFileSystem().processCallbacks();
|
|
}
|
|
|
|
m_editor->newWorld();
|
|
|
|
destroyAddCmpTreeNode(m_add_cmp_root.child);
|
|
|
|
for (auto* i : m_plugins) {
|
|
LUMIX_DELETE(m_allocator, i);
|
|
}
|
|
m_plugins.clear();
|
|
|
|
for (auto* i : m_gui_plugins) {
|
|
LUMIX_DELETE(m_allocator, i);
|
|
}
|
|
m_gui_plugins.clear();
|
|
|
|
PrefabSystem::destroyEditorPlugins(*this);
|
|
ASSERT(m_mouse_plugins.empty());
|
|
|
|
for (auto* i : m_add_cmp_plugins)
|
|
{
|
|
LUMIX_DELETE(m_allocator, i);
|
|
}
|
|
m_add_cmp_plugins.clear();
|
|
|
|
m_profiler_ui.reset();
|
|
m_asset_browser.reset();
|
|
m_property_grid.destroy();
|
|
m_log_ui.destroy();
|
|
ASSERT(!m_render_interface);
|
|
m_asset_compiler.reset();
|
|
m_editor.reset();
|
|
|
|
removeAction(&m_common_actions.save);
|
|
removeAction(&m_common_actions.undo);
|
|
removeAction(&m_common_actions.redo);
|
|
removeAction(&m_common_actions.del);
|
|
removeAction(&m_common_actions.cam_orbit);
|
|
removeAction(&m_common_actions.cam_forward);
|
|
removeAction(&m_common_actions.cam_backward);
|
|
removeAction(&m_common_actions.cam_right);
|
|
removeAction(&m_common_actions.cam_left);
|
|
removeAction(&m_common_actions.cam_up);
|
|
removeAction(&m_common_actions.cam_down);
|
|
|
|
for (Action* action : m_owned_actions) {
|
|
removeAction(action);
|
|
LUMIX_DELETE(m_allocator, action);
|
|
}
|
|
m_owned_actions.clear();
|
|
ASSERT(m_actions.empty());
|
|
m_actions.clear();
|
|
|
|
m_engine.reset();
|
|
|
|
jobs::shutdown();
|
|
}
|
|
|
|
|
|
void destroyAddCmpTreeNode(AddCmpTreeNode* node)
|
|
{
|
|
if (!node) return;
|
|
destroyAddCmpTreeNode(node->child);
|
|
destroyAddCmpTreeNode(node->next);
|
|
LUMIX_DELETE(m_allocator, node);
|
|
}
|
|
|
|
const char* getComponentIcon(ComponentType cmp_type) const override
|
|
{
|
|
auto iter = m_component_icons.find(cmp_type);
|
|
if (iter == m_component_icons.end()) return "";
|
|
return iter.value();
|
|
}
|
|
|
|
const char* getComponentTypeName(ComponentType cmp_type) const override
|
|
{
|
|
auto iter = m_component_labels.find(cmp_type);
|
|
if (iter == m_component_labels.end()) return "Unknown";
|
|
return iter.value().c_str();
|
|
}
|
|
|
|
|
|
const AddCmpTreeNode& getAddComponentTreeRoot() const override { return m_add_cmp_root; }
|
|
|
|
|
|
void addPlugin(IAddComponentPlugin& plugin)
|
|
{
|
|
int i = 0;
|
|
while (i < m_add_cmp_plugins.size() && compareString(plugin.getLabel(), m_add_cmp_plugins[i]->getLabel()) > 0)
|
|
{
|
|
++i;
|
|
}
|
|
m_add_cmp_plugins.insert(i, &plugin);
|
|
|
|
auto* node = LUMIX_NEW(m_allocator, AddCmpTreeNode);
|
|
copyString(node->label, plugin.getLabel());
|
|
node->plugin = &plugin;
|
|
insertAddCmpNode(m_add_cmp_root, node);
|
|
}
|
|
|
|
|
|
static void insertAddCmpNodeOrdered(AddCmpTreeNode& parent, AddCmpTreeNode* node)
|
|
{
|
|
if (!parent.child)
|
|
{
|
|
parent.child = node;
|
|
return;
|
|
}
|
|
if (compareString(parent.child->label, node->label) > 0)
|
|
{
|
|
node->next = parent.child;
|
|
parent.child = node;
|
|
return;
|
|
}
|
|
auto* i = parent.child;
|
|
while (i->next && compareString(i->next->label, node->label) < 0)
|
|
{
|
|
i = i->next;
|
|
}
|
|
node->next = i->next;
|
|
i->next = node;
|
|
}
|
|
|
|
|
|
void insertAddCmpNode(AddCmpTreeNode& parent, AddCmpTreeNode* node)
|
|
{
|
|
for (auto* i = parent.child; i; i = i->next)
|
|
{
|
|
if (!i->plugin && startsWith(node->label, i->label))
|
|
{
|
|
insertAddCmpNode(*i, node);
|
|
return;
|
|
}
|
|
}
|
|
const char* rest = node->label + stringLength(parent.label);
|
|
if (parent.label[0] != '\0') ++rest; // include '/'
|
|
const char* slash = find(rest, '/');
|
|
if (!slash)
|
|
{
|
|
insertAddCmpNodeOrdered(parent, node);
|
|
return;
|
|
}
|
|
auto* new_group = LUMIX_NEW(m_allocator, AddCmpTreeNode);
|
|
copyString(Span(new_group->label), StringView(node->label, u32(slash - node->label)));
|
|
insertAddCmpNodeOrdered(parent, new_group);
|
|
insertAddCmpNode(*new_group, node);
|
|
}
|
|
|
|
void registerComponent(const char* icon, ComponentType cmp_type, const char* label, ResourceType resource_type, const char* property) {
|
|
struct Plugin final : IAddComponentPlugin {
|
|
void onGUI(bool create_entity, bool from_filter, EntityPtr parent, WorldEditor& editor) override {
|
|
const char* last = reverseFind(label, '/');
|
|
last = last && !from_filter ? last + 1 : label;
|
|
if (last[0] == ' ') ++last;
|
|
if (!ImGui::BeginMenu(last)) return;
|
|
Path path;
|
|
bool create_empty = ImGui::MenuItem(ICON_FA_BROOM " Empty");
|
|
static FilePathHash selected_res_hash;
|
|
if (asset_browser->resourceList(path, selected_res_hash, resource_type, true) || create_empty) {
|
|
editor.beginCommandGroup("createEntityWithComponent");
|
|
if (create_entity) {
|
|
EntityRef entity = editor.addEntity();
|
|
editor.selectEntities(Span(&entity, 1), false);
|
|
}
|
|
|
|
const Array<EntityRef>& selected_entites = editor.getSelectedEntities();
|
|
editor.addComponent(selected_entites, type);
|
|
if (!create_empty) {
|
|
editor.setProperty(type, "", -1, property, editor.getSelectedEntities(), path);
|
|
}
|
|
if (parent.isValid()) editor.makeParent(parent, selected_entites[0]);
|
|
editor.endCommandGroup();
|
|
editor.lockGroupCommand();
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
|
|
const char* getLabel() const override { return label; }
|
|
|
|
PropertyGrid* property_grid;
|
|
AssetBrowser* asset_browser;
|
|
ComponentType type;
|
|
ResourceType resource_type;
|
|
StaticString<64> property;
|
|
char label[50];
|
|
};
|
|
|
|
Plugin* plugin = LUMIX_NEW(m_allocator, Plugin);
|
|
plugin->property_grid = m_property_grid.get();
|
|
plugin->asset_browser = m_asset_browser.get();
|
|
plugin->type = cmp_type;
|
|
plugin->property = property;
|
|
plugin->resource_type = resource_type;
|
|
copyString(plugin->label, label);
|
|
addPlugin(*plugin);
|
|
|
|
m_component_labels.insert(plugin->type, String(label, m_allocator));
|
|
if (icon && icon[0]) {
|
|
m_component_icons.insert(plugin->type, icon);
|
|
}
|
|
}
|
|
|
|
void registerComponent(const char* icon, const char* id, IAddComponentPlugin& plugin) override {
|
|
addPlugin(plugin);
|
|
m_component_labels.insert(reflection::getComponentType(id), String(plugin.getLabel(), m_allocator));
|
|
if (icon && icon[0]) {
|
|
m_component_icons.insert(reflection::getComponentType(id), icon);
|
|
}
|
|
}
|
|
|
|
void registerComponent(const char* icon, ComponentType type, const char* label)
|
|
{
|
|
struct Plugin final : IAddComponentPlugin
|
|
{
|
|
void onGUI(bool create_entity, bool from_filter, EntityPtr parent, WorldEditor& editor) override
|
|
{
|
|
const char* last = reverseFind(label, '/');
|
|
last = last && !from_filter ? last + 1 : label;
|
|
if (last[0] == ' ') ++last;
|
|
if (ImGui::MenuItem(last))
|
|
{
|
|
editor.beginCommandGroup("createEntityWithComponent");
|
|
if (create_entity)
|
|
{
|
|
EntityRef entity = editor.addEntity();
|
|
editor.selectEntities(Span(&entity, 1), false);
|
|
}
|
|
|
|
const Array<EntityRef>& selected = editor.getSelectedEntities();
|
|
editor.addComponent(selected, type);
|
|
if (parent.isValid()) editor.makeParent(parent, selected[0]);
|
|
editor.endCommandGroup();
|
|
editor.lockGroupCommand();
|
|
}
|
|
}
|
|
|
|
const char* getLabel() const override { return label; }
|
|
|
|
PropertyGrid* property_grid;
|
|
ComponentType type;
|
|
char label[64];
|
|
};
|
|
|
|
Plugin* plugin = LUMIX_NEW(m_allocator, Plugin);
|
|
plugin->property_grid = m_property_grid.get();
|
|
plugin->type = type;
|
|
copyString(plugin->label, label);
|
|
addPlugin(*plugin);
|
|
|
|
m_component_labels.insert(plugin->type, String(label, m_allocator));
|
|
if (icon && icon[0]) {
|
|
m_component_icons.insert(plugin->type, icon);
|
|
}
|
|
}
|
|
|
|
|
|
const Array<Action*>& getActions() override { return m_actions; }
|
|
|
|
|
|
void guiBeginFrame()
|
|
{
|
|
PROFILE_FUNCTION();
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
updateIMGUIMonitors();
|
|
const os::Rect rect = os::getWindowClientRect(m_main_window);
|
|
if (rect.width > 0 && rect.height > 0) {
|
|
io.DisplaySize = ImVec2(float(rect.width), float(rect.height));
|
|
}
|
|
else if(io.DisplaySize.x <= 0) {
|
|
io.DisplaySize.x = 800;
|
|
io.DisplaySize.y = 600;
|
|
}
|
|
io.DeltaTime = m_engine->getLastTimeDelta();
|
|
|
|
if (!m_cursor_clipped) {
|
|
const os::Point cp = os::getMouseScreenPos();
|
|
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
|
io.AddMousePosEvent((float)cp.x, (float)cp.y);
|
|
}
|
|
else {
|
|
const os::Rect screen_rect = os::getWindowScreenRect(m_main_window);
|
|
io.AddMousePosEvent((float)cp.x - screen_rect.left, (float)cp.y - screen_rect.top);
|
|
}
|
|
}
|
|
|
|
const ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor();
|
|
ImGui::NewFrame();
|
|
if (!m_cursor_clipped) {
|
|
static ImGuiMouseCursor last_cursor = ImGuiMouseCursor_COUNT;
|
|
if (imgui_cursor != last_cursor) {
|
|
switch (imgui_cursor) {
|
|
case ImGuiMouseCursor_Arrow: os::setCursor(os::CursorType::DEFAULT); break;
|
|
case ImGuiMouseCursor_ResizeNS: os::setCursor(os::CursorType::SIZE_NS); break;
|
|
case ImGuiMouseCursor_ResizeEW: os::setCursor(os::CursorType::SIZE_WE); break;
|
|
case ImGuiMouseCursor_ResizeNWSE: os::setCursor(os::CursorType::SIZE_NWSE); break;
|
|
case ImGuiMouseCursor_TextInput: os::setCursor(os::CursorType::TEXT_INPUT); break;
|
|
default: os::setCursor(os::CursorType::DEFAULT); break;
|
|
}
|
|
last_cursor = imgui_cursor;
|
|
}
|
|
}
|
|
ImGui::PushFont(m_font);
|
|
|
|
if (os::getFocused() != m_main_window && m_cursor_clipped) unclipMouseCursor();
|
|
}
|
|
|
|
u32 getDockspaceID() const override {
|
|
return m_dockspace_id;
|
|
}
|
|
|
|
void guiEndFrame()
|
|
{
|
|
PROFILE_FUNCTION();
|
|
if (m_is_welcome_screen_open) {
|
|
m_dockspace_id = ImGui::DockSpaceOverViewport(ImGui::GetMainViewport());
|
|
guiWelcomeScreen();
|
|
}
|
|
else {
|
|
mainMenu();
|
|
|
|
m_dockspace_id = ImGui::DockSpaceOverViewport(ImGui::GetMainViewport());
|
|
m_asset_compiler->onGUI();
|
|
guiAllActions();
|
|
guiEntityList();
|
|
guiSaveAsDialog();
|
|
for (i32 i = m_gui_plugins.size() - 1; i >= 0; --i) {
|
|
GUIPlugin* win = m_gui_plugins[i];
|
|
win->onGUI();
|
|
}
|
|
|
|
m_settings.onGUI();
|
|
guiExportData();
|
|
}
|
|
ImGui::PopFont();
|
|
ImGui::Render();
|
|
ImGui::UpdatePlatformWindows();
|
|
|
|
for (auto* plugin : m_gui_plugins)
|
|
{
|
|
plugin->guiEndFrame();
|
|
}
|
|
}
|
|
|
|
void showGizmos() {
|
|
const Array<EntityRef>& ents = m_editor->getSelectedEntities();
|
|
if (ents.empty()) return;
|
|
|
|
World* world = m_editor->getWorld();
|
|
|
|
WorldView& view = m_editor->getView();
|
|
if (ents.size() > 1) {
|
|
DVec3 min(FLT_MAX), max(-FLT_MAX);
|
|
for (EntityRef e : ents) {
|
|
const DVec3 p = world->getPosition(e);
|
|
min = minimum(p, min);
|
|
max = maximum(p, max);
|
|
}
|
|
|
|
addCube(view, min, max, 0xffffff00);
|
|
return;
|
|
}
|
|
|
|
for (ComponentUID cmp = world->getFirstComponent(ents[0]); cmp.isValid(); cmp = world->getNextComponent(cmp)) {
|
|
for (auto* plugin : m_plugins) {
|
|
if (plugin->showGizmo(view, cmp)) break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void updateGizmoOffset() {
|
|
const Array<EntityRef>& ents = m_editor->getSelectedEntities();
|
|
if (ents.size() != 1) {
|
|
m_gizmo_config.offset = Vec3::ZERO;
|
|
return;
|
|
}
|
|
static EntityPtr last_selected = INVALID_ENTITY;
|
|
if (last_selected != ents[0]) {
|
|
m_gizmo_config.offset = Vec3::ZERO;
|
|
last_selected = ents[0];
|
|
}
|
|
}
|
|
|
|
void update() {
|
|
PROFILE_FUNCTION();
|
|
profiler::blockColor(0x7f, 0x7f, 0x7f);
|
|
|
|
updateGizmoOffset();
|
|
|
|
for (i32 i = m_deferred_destroy_windows.size() - 1; i >= 0; --i) {
|
|
--m_deferred_destroy_windows[i].counter;
|
|
if (m_deferred_destroy_windows[i].counter == 0) {
|
|
os::destroyWindow(m_deferred_destroy_windows[i].window);
|
|
m_deferred_destroy_windows.swapAndPop(i);
|
|
}
|
|
}
|
|
|
|
if (m_watched_plugin.reload_request) tryReloadPlugin();
|
|
|
|
guiBeginFrame();
|
|
m_asset_compiler->update();
|
|
m_editor->update();
|
|
showGizmos();
|
|
|
|
m_engine->update(*m_editor->getWorld());
|
|
|
|
++m_fps_frame;
|
|
if (m_fps_timer.getTimeSinceTick() > 1.0f) {
|
|
m_fps = m_fps_frame / m_fps_timer.tick();
|
|
m_fps_frame = 0;
|
|
}
|
|
|
|
if (m_deferred_game_mode_exit) {
|
|
m_deferred_game_mode_exit = false;
|
|
m_editor->toggleGameMode();
|
|
}
|
|
|
|
float time_delta = m_engine->getLastTimeDelta();
|
|
for (auto* plugin : m_gui_plugins) {
|
|
plugin->update(time_delta);
|
|
}
|
|
|
|
if (m_settings.getTimeSinceLastSave() > 30.f) saveSettings();
|
|
|
|
guiEndFrame();
|
|
}
|
|
|
|
|
|
void extractBundled() {
|
|
#ifdef _WIN32
|
|
HRSRC hrsrc = FindResourceA(GetModuleHandle(NULL), MAKEINTRESOURCE(102), "TAR");
|
|
if (!hrsrc) return;
|
|
HGLOBAL hglobal = LoadResource(GetModuleHandle(NULL), hrsrc);
|
|
if (!hglobal) return;
|
|
const DWORD res_size = SizeofResource(GetModuleHandle(NULL), hrsrc);
|
|
if (res_size == 0) return;
|
|
const void* res_mem = LockResource(hglobal);
|
|
if (!res_mem) return;
|
|
|
|
TCHAR exe_path[MAX_PATH];
|
|
GetModuleFileNameA(NULL, exe_path, MAX_PATH);
|
|
|
|
// TODO extract only nonexistent files
|
|
InputMemoryStream str(res_mem, res_size);
|
|
|
|
TarHeader header;
|
|
while (str.getPosition() < str.size()) {
|
|
const u8* ptr = (const u8*)str.getData() + str.getPosition();
|
|
str.read(&header, sizeof(header));
|
|
u32 size;
|
|
fromCStringOctal(StringView(header.size, sizeof(header.size)), size);
|
|
if (header.name[0] && (header.typeflag == 0 || header.typeflag == '0')) {
|
|
const Path path(m_engine->getFileSystem().getBasePath(), "/", header.name);
|
|
char dir[MAX_PATH];
|
|
copyString(Span(dir), Path::getDir(path));
|
|
if (!os::makePath(dir)) logError("");
|
|
if (!os::fileExists(path)) {
|
|
os::OutputFile file;
|
|
if (file.open(path.c_str())) {
|
|
if (!file.write(ptr + 512, size)) {
|
|
logError("Failed to write ", path);
|
|
}
|
|
file.close();
|
|
}
|
|
else {
|
|
logError("Failed to extract ", path);
|
|
}
|
|
}
|
|
}
|
|
|
|
str.setPosition(str.getPosition() + (512 - str.getPosition() % 512) % 512);
|
|
str.setPosition(str.getPosition() + size + (512 - size % 512) % 512);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
void initDefaultWorld() {
|
|
m_editor->beginCommandGroup("initWorld");
|
|
EntityRef env = m_editor->addEntity();
|
|
m_editor->setEntityName(env, "environment");
|
|
ComponentType env_cmp_type = reflection::getComponentType("environment");
|
|
ComponentType lua_script_cmp_type = reflection::getComponentType("lua_script");
|
|
Span<EntityRef> entities(&env, 1);
|
|
m_editor->addComponent(entities, env_cmp_type);
|
|
m_editor->addComponent(entities, lua_script_cmp_type);
|
|
Quat rot;
|
|
rot.fromEuler(Vec3(degreesToRadians(45.f), 0, 0));
|
|
m_editor->setEntitiesRotations(&env, &rot, 1);
|
|
const ComponentUID cmp = m_editor->getWorld()->getComponent(env, lua_script_cmp_type);
|
|
m_editor->addArrayPropertyItem(cmp, "scripts");
|
|
m_editor->setProperty(lua_script_cmp_type, "scripts", 0, "Path", entities, Path("pipelines/atmo.lua"));
|
|
m_editor->endCommandGroup();
|
|
}
|
|
|
|
void tryLoadWorld(const Path& path, bool additive) override {
|
|
if (!additive && m_editor->isWorldChanged()) {
|
|
m_world_to_load = path;
|
|
m_confirm_load = true;
|
|
}
|
|
else {
|
|
loadWorld(path, additive);
|
|
}
|
|
}
|
|
|
|
|
|
void loadWorld(const Path& path, bool additive) {
|
|
FileSystem& fs = m_engine->getFileSystem();
|
|
OutputMemoryStream data(m_allocator);
|
|
|
|
if (!fs.getContentSync(path, data)) {
|
|
logError("Failed to read ", path);
|
|
m_editor->newWorld();
|
|
return;
|
|
}
|
|
|
|
InputMemoryStream blob(data);
|
|
m_editor->loadWorld(blob, path.c_str(), additive);
|
|
}
|
|
|
|
void guiWelcomeScreen()
|
|
{
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings;
|
|
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
|
ImGui::SetNextWindowPos(viewport->WorkPos);
|
|
ImGui::SetNextWindowSize(viewport->WorkSize);
|
|
ImGui::SetNextWindowViewport(viewport->ID);
|
|
if (ImGui::Begin("Welcome", nullptr, flags)) {
|
|
#ifdef _WIN32
|
|
alignGUIRight([&](){
|
|
if (ImGuiEx::IconButton(ICON_FA_WINDOW_MINIMIZE, nullptr)) os::minimizeWindow(m_engine->getWindowHandle());
|
|
ImGui::SameLine();
|
|
if (os::isMaximized(m_engine->getWindowHandle())) {
|
|
if (ImGuiEx::IconButton(ICON_FA_WINDOW_RESTORE, nullptr)) os::restore(m_engine->getWindowHandle());
|
|
}
|
|
else {
|
|
if (ImGuiEx::IconButton(ICON_FA_WINDOW_MAXIMIZE, nullptr)) os::maximizeWindow(m_engine->getWindowHandle());
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGuiEx::IconButton(ICON_FA_WINDOW_CLOSE, nullptr)) exit();
|
|
});
|
|
#endif
|
|
|
|
ImGui::Text("Welcome to Lumix Studio");
|
|
|
|
ImVec2 half_size = ImGui::GetContentRegionAvail();
|
|
half_size.x = half_size.x * 0.5f - ImGui::GetStyle().FramePadding.x;
|
|
half_size.y *= 0.99f;
|
|
auto right_pos = ImGui::GetCursorPos();
|
|
right_pos.x += half_size.x + ImGui::GetStyle().FramePadding.x;
|
|
if (ImGui::BeginChild("left", half_size, true))
|
|
{
|
|
ImGui::Text("Working directory: %s", m_engine->getFileSystem().getBasePath());
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Change...")) {
|
|
char dir[MAX_PATH];
|
|
if (os::getOpenDirectory(Span(dir), m_engine->getFileSystem().getBasePath())) {
|
|
os::OutputFile cfg_file;
|
|
if (cfg_file.open(".lumixuser")) {
|
|
cfg_file << dir;
|
|
cfg_file.close();
|
|
}
|
|
m_engine->getFileSystem().setBasePath(dir);
|
|
extractBundled();
|
|
m_editor->loadProject();
|
|
m_asset_compiler->onBasePathChanged();
|
|
m_engine->getResourceManager().reloadAll();
|
|
}
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::Button("New world")) {
|
|
initDefaultWorld();
|
|
m_is_welcome_screen_open = false;
|
|
}
|
|
ImGui::Text("Open world:");
|
|
ImGui::Indent();
|
|
forEachWorld([&](const Path& path){
|
|
if (ImGui::MenuItem(path.c_str())) {
|
|
loadWorld(path, false);
|
|
m_is_welcome_screen_open = false;
|
|
}
|
|
});
|
|
ImGui::Unindent();
|
|
}
|
|
ImGui::EndChild();
|
|
|
|
ImGui::SetCursorPos(right_pos);
|
|
|
|
if (ImGui::BeginChild("right", half_size, true))
|
|
{
|
|
ImGui::Text("Using NVidia PhysX");
|
|
|
|
if (ImGui::Button("Wiki"))
|
|
{
|
|
os::shellExecuteOpen("https://github.com/nem0/LumixEngine/wiki");
|
|
}
|
|
|
|
if (ImGui::Button("Show major releases"))
|
|
{
|
|
os::shellExecuteOpen("https://github.com/nem0/LumixEngine/releases");
|
|
}
|
|
|
|
if (ImGui::Button("Show latest commits"))
|
|
{
|
|
os::shellExecuteOpen("https://github.com/nem0/LumixEngine/commits/master");
|
|
}
|
|
|
|
if (ImGui::Button("Show issues"))
|
|
{
|
|
os::shellExecuteOpen("https://github.com/nem0/lumixengine/issues");
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
void save() {
|
|
if (m_editor->isGameMode()) {
|
|
logError("Could not save while the game is running");
|
|
return;
|
|
}
|
|
|
|
World* world = m_editor->getWorld();
|
|
const Array<World::Partition>& partitions = world->getPartitions();
|
|
|
|
if (partitions.size() == 1 && partitions[0].name[0] == '\0') {
|
|
saveAs();
|
|
}
|
|
else {
|
|
for (const World::Partition& partition : partitions) {
|
|
m_editor->savePartition(partition.handle);
|
|
}
|
|
}
|
|
}
|
|
|
|
void guiSaveAsDialog() {
|
|
if (m_file_selector.gui("Save world as", &m_show_save_world_ui, "unv", true)) {
|
|
ASSERT(!m_editor->isGameMode());
|
|
World* world = m_editor->getWorld();
|
|
World::PartitionHandle active_partition_handle = world->getActivePartition();
|
|
World::Partition& active_partition = world->getPartition(active_partition_handle);
|
|
copyString(active_partition.name, m_file_selector.getPath());
|
|
m_editor->savePartition(active_partition_handle);
|
|
}
|
|
}
|
|
|
|
|
|
void saveAs() {
|
|
if (m_editor->isGameMode()) {
|
|
logError("Can not save while the game is running");
|
|
return;
|
|
}
|
|
|
|
m_show_save_world_ui = true;
|
|
}
|
|
|
|
void exit() {
|
|
if (m_editor->isWorldChanged()) {
|
|
m_confirm_exit = true;
|
|
}
|
|
else {
|
|
m_finished = true;
|
|
}
|
|
}
|
|
|
|
void newWorld() {
|
|
if (m_editor->isWorldChanged()) {
|
|
m_confirm_new = true;
|
|
}
|
|
else {
|
|
m_editor->newWorld();
|
|
initDefaultWorld();
|
|
}
|
|
}
|
|
|
|
|
|
GUIPlugin* getFocusedWindow() {
|
|
for (GUIPlugin* win : m_gui_plugins) {
|
|
if (win->hasFocus()) return win;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Gizmo::Config& getGizmoConfig() override { return m_gizmo_config; }
|
|
|
|
void clipMouseCursor() override { m_cursor_clipped = true; }
|
|
void unclipMouseCursor() override {
|
|
os::clipCursor(os::INVALID_WINDOW, {});
|
|
m_cursor_clipped = false;
|
|
}
|
|
bool isMouseCursorClipped() const override {return m_cursor_clipped; }
|
|
|
|
void setMouseClipRect(os::WindowHandle win, const os::Rect &screen_rect) override {
|
|
if (!m_cursor_clipped) return;
|
|
os::clipCursor(win, screen_rect);
|
|
}
|
|
|
|
void addEntity() {
|
|
const EntityRef e = m_editor->addEntity();
|
|
m_editor->selectEntities(Span(&e, 1), false);
|
|
}
|
|
|
|
void undo() { m_editor->undo(); }
|
|
void redo() { m_editor->redo(); }
|
|
void copy() { m_editor->copyEntities(); }
|
|
void paste() { m_editor->pasteEntities(); }
|
|
void duplicate() { m_editor->duplicateEntities(); }
|
|
void setLocalCoordSystem() { getGizmoConfig().coord_system = Gizmo::Config::LOCAL; }
|
|
void setGlobalCoordSystem() { getGizmoConfig().coord_system = Gizmo::Config::GLOBAL; }
|
|
void toggleSettings() { m_settings.m_is_open = !m_settings.m_is_open; }
|
|
bool areSettingsOpen() const { return m_settings.m_is_open; }
|
|
void toggleEntityList() { m_is_entity_list_open = !m_is_entity_list_open; }
|
|
bool isEntityListOpen() const { return m_is_entity_list_open; }
|
|
int getExitCode() const override { return m_exit_code; }
|
|
|
|
DirSelector& getDirSelector() override {
|
|
return m_dir_selector;
|
|
}
|
|
|
|
FileSelector& getFileSelector() override {
|
|
return m_file_selector;
|
|
}
|
|
|
|
AssetBrowser& getAssetBrowser() override
|
|
{
|
|
ASSERT(m_asset_browser.get());
|
|
return *m_asset_browser;
|
|
}
|
|
AssetCompiler& getAssetCompiler() override
|
|
{
|
|
ASSERT(m_asset_compiler.get());
|
|
return *m_asset_compiler;
|
|
}
|
|
PropertyGrid& getPropertyGrid() override
|
|
{
|
|
ASSERT(m_property_grid.get());
|
|
return *m_property_grid;
|
|
}
|
|
LogUI& getLogUI() override
|
|
{
|
|
ASSERT(m_log_ui.get());
|
|
return *m_log_ui;
|
|
}
|
|
|
|
void nextFrame() { m_engine->nextFrame(); }
|
|
void pauseGame() { m_engine->pause(!m_engine->isPaused()); }
|
|
void toggleGameMode() { m_editor->toggleGameMode(); }
|
|
void setTranslateGizmoMode() { getGizmoConfig().mode = Gizmo::Config::TRANSLATE; }
|
|
void setRotateGizmoMode() { getGizmoConfig().mode = Gizmo::Config::ROTATE; }
|
|
void setScaleGizmoMode() { getGizmoConfig().mode = Gizmo::Config::SCALE; }
|
|
|
|
|
|
void makeParent()
|
|
{
|
|
const auto& entities = m_editor->getSelectedEntities();
|
|
if (entities.size() == 2) {
|
|
m_editor->makeParent(entities[0], entities[1]);
|
|
}
|
|
}
|
|
|
|
|
|
void unparent()
|
|
{
|
|
const auto& entities = m_editor->getSelectedEntities();
|
|
if (entities.size() != 1) return;
|
|
m_editor->makeParent(INVALID_ENTITY, entities[0]);
|
|
}
|
|
|
|
|
|
void snapDown() override {
|
|
const Array<EntityRef>& selected = m_editor->getSelectedEntities();
|
|
if (selected.empty()) return;
|
|
|
|
Array<DVec3> new_positions(m_allocator);
|
|
World* world = m_editor->getWorld();
|
|
|
|
for (EntityRef entity : selected) {
|
|
const DVec3 origin = world->getPosition(entity);
|
|
auto hit = getRenderInterface()->castRay(*world, Ray{origin, Vec3(0, -1, 0)}, entity);
|
|
if (hit.is_hit) {
|
|
new_positions.push(origin + Vec3(0, -hit.t, 0));
|
|
}
|
|
else {
|
|
hit = getRenderInterface()->castRay(*world, Ray{origin, Vec3(0, 1, 0)}, entity);
|
|
if (hit.is_hit) {
|
|
new_positions.push(origin + Vec3(0, hit.t, 0));
|
|
}
|
|
else {
|
|
new_positions.push(world->getPosition(entity));
|
|
}
|
|
}
|
|
}
|
|
m_editor->setEntitiesPositions(&selected[0], &new_positions[0], new_positions.size());
|
|
}
|
|
|
|
void autosnapDown()
|
|
{
|
|
Gizmo::Config& cfg = getGizmoConfig();
|
|
cfg.setAutosnapDown(!cfg.isAutosnapDown());
|
|
}
|
|
|
|
|
|
void destroySelectedEntity()
|
|
{
|
|
auto& selected_entities = m_editor->getSelectedEntities();
|
|
if (selected_entities.empty()) return;
|
|
m_editor->destroyEntities(&selected_entities[0], selected_entities.size());
|
|
}
|
|
|
|
|
|
void removeAction(Action* action) override
|
|
{
|
|
m_actions.eraseItem(action);
|
|
m_window_actions.eraseItem(action);
|
|
m_tools_actions.eraseItem(action);
|
|
}
|
|
|
|
void addToolAction(Action* action) override {
|
|
addAction(action);
|
|
m_tools_actions.push(action);
|
|
}
|
|
|
|
void addWindowAction(Action* action) override
|
|
{
|
|
addAction(action);
|
|
for (int i = 0; i < m_window_actions.size(); ++i)
|
|
{
|
|
if (compareString(m_window_actions[i]->label_short, action->label_short) > 0)
|
|
{
|
|
m_window_actions.insert(i, action);
|
|
return;
|
|
}
|
|
}
|
|
m_window_actions.push(action);
|
|
}
|
|
|
|
void addAction(Action* action) override {
|
|
for (int i = 0; i < m_actions.size(); ++i) {
|
|
if (compareString(m_actions[i]->label_long, action->label_long) > 0) {
|
|
m_actions.insert(i, action);
|
|
return;
|
|
}
|
|
}
|
|
m_actions.push(action);
|
|
}
|
|
|
|
template <void (StudioAppImpl::*Func)()>
|
|
Action& addAction(const char* label_short, const char* label_long, const char* name, const char* font_icon = "")
|
|
{
|
|
Action* a = LUMIX_NEW(m_allocator, Action);
|
|
a->init(label_short, label_long, name, font_icon, Action::IMGUI_PRIORITY);
|
|
a->func.bind<Func>(this);
|
|
addAction(a);
|
|
m_owned_actions.push(a);
|
|
return *a;
|
|
}
|
|
|
|
|
|
template <void (StudioAppImpl::*Func)()>
|
|
void addAction(const char* label_short,
|
|
const char* label_long,
|
|
const char* name,
|
|
const char* font_icon,
|
|
os::Keycode shortcut,
|
|
Action::Modifiers modifiers)
|
|
{
|
|
Action* a = LUMIX_NEW(m_allocator, Action);
|
|
a->init(label_short, label_long, name, font_icon, shortcut, modifiers, Action::IMGUI_PRIORITY);
|
|
a->func.bind<Func>(this);
|
|
m_owned_actions.push(a);
|
|
addAction(a);
|
|
}
|
|
|
|
|
|
Action* getAction(const char* name) override
|
|
{
|
|
for (auto* a : m_actions)
|
|
{
|
|
if (equalStrings(a->name, name)) return a;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
static void showAddComponentNode(const StudioApp::AddCmpTreeNode* node, const TextFilter& filter, EntityPtr parent, WorldEditor& editor)
|
|
{
|
|
if (!node) return;
|
|
|
|
if (filter.isActive()) {
|
|
if (!node->plugin) showAddComponentNode(node->child, filter, parent, editor);
|
|
else if (filter.pass(node->plugin->getLabel())) node->plugin->onGUI(true, true, parent, editor);
|
|
showAddComponentNode(node->next, filter, parent, editor);
|
|
return;
|
|
}
|
|
|
|
if (node->plugin) {
|
|
node->plugin->onGUI(true, false, parent, editor);
|
|
showAddComponentNode(node->next, filter, parent, editor);
|
|
return;
|
|
}
|
|
|
|
const char* last = reverseFind(node->label, '/');
|
|
last = last ? last + 1 : node->label;
|
|
if (last[0] == ' ') ++last;
|
|
if (ImGui::BeginMenu(last)) {
|
|
showAddComponentNode(node->child, filter, parent, editor);
|
|
ImGui::EndMenu();
|
|
}
|
|
showAddComponentNode(node->next, filter, parent, editor);
|
|
}
|
|
|
|
|
|
void onCreateEntityWithComponentGUI(EntityPtr parent)
|
|
{
|
|
char shortcut[64] = "";
|
|
const Action* create_entity_action = getAction("createEntity");
|
|
if (create_entity_action) getShortcut(*create_entity_action, Span(shortcut));
|
|
|
|
if (ImGui::MenuItem("Create empty", shortcut)) {
|
|
m_editor->beginCommandGroup("create_child");
|
|
const EntityRef e = m_editor->addEntity();
|
|
m_editor->selectEntities(Span(&e, 1), false);
|
|
if (parent.isValid()) m_editor->makeParent(parent, e);
|
|
m_editor->endCommandGroup();
|
|
}
|
|
|
|
m_component_filter.gui("Filter", 150);
|
|
|
|
showAddComponentNode(m_add_cmp_root.child, m_component_filter, parent, *m_editor);
|
|
}
|
|
|
|
|
|
void entityMenu()
|
|
{
|
|
if (!ImGui::BeginMenu("Entity")) return;
|
|
|
|
const auto& selected_entities = m_editor->getSelectedEntities();
|
|
bool is_any_entity_selected = !selected_entities.empty();
|
|
if (ImGuiEx::BeginMenuEx("Create", ICON_FA_PLUS_SQUARE))
|
|
{
|
|
onCreateEntityWithComponentGUI(INVALID_ENTITY);
|
|
ImGui::EndMenu();
|
|
}
|
|
menuItem("delete", is_any_entity_selected);
|
|
|
|
if (ImGui::BeginMenu("Save as prefab", selected_entities.size() == 1)) {
|
|
bool selected = m_file_selector.gui(false, "fab");
|
|
selected = ImGui::Button(ICON_FA_SAVE " Save") || selected;
|
|
if (selected) {
|
|
char filename[MAX_PATH];
|
|
Path::normalize(m_file_selector.getPath(), filename);
|
|
EntityRef entity = selected_entities[0];
|
|
m_editor->getPrefabSystem().savePrefab(entity, Path(filename));
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
menuItem("makeParent", selected_entities.size() == 2);
|
|
bool can_unparent = selected_entities.size() == 1 && m_editor->getWorld()->getParent(selected_entities[0]).isValid();
|
|
menuItem("unparent", can_unparent);
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
void menuItem(const char* name, bool enabled) {
|
|
Action* action = getAction(name);
|
|
if (!action) {
|
|
ASSERT(false);
|
|
return;
|
|
}
|
|
if (Lumix::menuItem(*action, enabled)) action->func.invoke();
|
|
}
|
|
|
|
void editMenu()
|
|
{
|
|
if (!ImGui::BeginMenu("Edit")) return;
|
|
|
|
bool is_any_entity_selected = !m_editor->getSelectedEntities().empty();
|
|
menuItem("undo", m_editor->canUndo());
|
|
menuItem("redo", m_editor->canRedo());
|
|
ImGui::Separator();
|
|
menuItem("copy", is_any_entity_selected);
|
|
menuItem("paste", m_editor->canPasteEntities());
|
|
menuItem("duplicate", is_any_entity_selected);
|
|
ImGui::Separator();
|
|
menuItem("setTranslateGizmoMode", true);
|
|
menuItem("setRotateGizmoMode", true);
|
|
menuItem("setScaleGizmoMode", true);
|
|
menuItem("setLocalCoordSystem", true);
|
|
menuItem("setGlobalCoordSystem", true);
|
|
if (ImGuiEx::BeginMenuEx("View", ICON_FA_CAMERA, true))
|
|
{
|
|
menuItem("toggleProjection", true);
|
|
menuItem("viewTop", true);
|
|
menuItem("viewFront", true);
|
|
menuItem("viewSide", true);
|
|
ImGui::EndMenu();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
template <typename T>
|
|
void forEachWorld(const T& f) {
|
|
const HashMap<FilePathHash, AssetCompiler::ResourceItem>& resources = m_asset_compiler->lockResources();
|
|
ResourceType WORLD_TYPE("world");
|
|
for (const AssetCompiler::ResourceItem& ri : resources) {
|
|
if (ri.type != WORLD_TYPE) continue;
|
|
f(ri.path);
|
|
}
|
|
m_asset_compiler->unlockResources();
|
|
}
|
|
|
|
|
|
void fileMenu()
|
|
{
|
|
if (!ImGui::BeginMenu("File")) return;
|
|
|
|
menuItem("newWorld", true);
|
|
const Array<World::Partition>& partitions = m_editor->getWorld()->getPartitions();
|
|
auto open_ui = [&](const char* label, bool additive){
|
|
if (ImGui::BeginMenu(label)) {
|
|
m_open_filter.gui("Filter", 150);
|
|
|
|
forEachWorld([&](const Path& path){
|
|
if (m_open_filter.pass(path.c_str()) && ImGui::MenuItem(path.c_str())) {
|
|
tryLoadWorld(path, additive);
|
|
}
|
|
});
|
|
ImGui::EndMenu();
|
|
}
|
|
};
|
|
open_ui("Open", false);
|
|
const bool can_load_additive = partitions.size() != 1 || partitions[0].name[0] != '\0';
|
|
if (can_load_additive) {
|
|
open_ui("Open additive", true);
|
|
}
|
|
else {
|
|
if (ImGui::BeginMenu("Open additive")) {
|
|
ImGui::TextUnformatted("Please save current partition first");
|
|
ImGui::EndMenu();
|
|
}
|
|
}
|
|
menuItem("save", !m_editor->isGameMode());
|
|
menuItem("saveAs", !m_editor->isGameMode());
|
|
menuItem("exit", true);
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
|
|
void toolsMenu()
|
|
{
|
|
if (!ImGui::BeginMenu("Tools")) return;
|
|
|
|
bool is_any_entity_selected = !m_editor->getSelectedEntities().empty();
|
|
menuItem("focus_asset_search", true);
|
|
menuItem("snapDown", is_any_entity_selected);
|
|
menuItem("autosnapDown", true);
|
|
menuItem("export_game", true);
|
|
for (Action* action : m_tools_actions) {
|
|
if (Lumix::menuItem(*action, true)) {
|
|
action->func.invoke();
|
|
}
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
void viewMenu() {
|
|
if (!ImGui::BeginMenu("View")) return;
|
|
|
|
menuItem("entityList", true);
|
|
menuItem("settings", true);
|
|
ImGui::Separator();
|
|
for (Action* action : m_window_actions) {
|
|
if (Lumix::menuItem(*action, true)) action->func.invoke();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
void mainMenu()
|
|
{
|
|
if (m_confirm_exit) {
|
|
openCenterStrip("Confirm##confirm_exit");
|
|
m_confirm_exit = false;
|
|
}
|
|
if (beginCenterStrip("Confirm##confirm_exit", 6)) {
|
|
ImGui::NewLine();
|
|
ImGuiEx::TextCentered("All unsaved changes will be lost, do you want to continue?");
|
|
ImGui::NewLine();
|
|
alignGUICenter([&](){
|
|
if (ImGui::Button("Continue")) {
|
|
m_finished = true;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup();
|
|
});
|
|
endCenterStrip();
|
|
}
|
|
|
|
if (m_confirm_destroy_partition) {
|
|
ImGui::OpenPopup("Confirm##confirm_destroy_partition");
|
|
m_confirm_destroy_partition = false;
|
|
}
|
|
if (ImGui::BeginPopupModal("Confirm##confirm_destroy_partition")) {
|
|
ImGui::Text("All unsaved changes will be lost, do you want to continue?");
|
|
if (ImGui::Button("Continue")) {
|
|
m_editor->destroyWorldPartition(m_partition_to_destroy);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup();
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
if (m_confirm_new) {
|
|
openCenterStrip("Confirm##confirm_new");
|
|
m_confirm_new = false;
|
|
}
|
|
if (beginCenterStrip("Confirm##confirm_new", 6)) {
|
|
ImGui::NewLine();
|
|
ImGuiEx::TextCentered("All unsaved changes will be lost, do you want to continue?");
|
|
ImGui::NewLine();
|
|
alignGUICenter([&](){
|
|
if (ImGui::Button("Continue")) {
|
|
m_editor->newWorld();
|
|
initDefaultWorld();
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup();
|
|
});
|
|
endCenterStrip();
|
|
}
|
|
|
|
if (m_confirm_load) {
|
|
openCenterStrip("Confirm");
|
|
m_confirm_load = false;
|
|
}
|
|
if (beginCenterStrip("Confirm", 6)) {
|
|
ImGui::NewLine();
|
|
ImGuiEx::TextCentered("All unsaved changes will be lost, do you want to continue?");
|
|
ImGui::NewLine();
|
|
alignGUICenter([&](){
|
|
if (ImGui::Button("Continue")) {
|
|
loadWorld(m_world_to_load, false);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup();
|
|
});
|
|
endCenterStrip();
|
|
}
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 8));
|
|
|
|
if (ImGui::BeginMainMenuBar()) {
|
|
if(m_render_interface && m_render_interface->isValid(m_logo)) {
|
|
ImGui::Image(*(void**)m_logo, ImVec2(ImGui::GetFrameHeight(), ImGui::GetFrameHeight()));
|
|
}
|
|
|
|
ImGui::PopStyleVar(2);
|
|
const ImVec2 menu_min = ImGui::GetCursorPos();
|
|
ImGui::SetNextItemAllowOverlap();
|
|
ImGui::InvisibleButton("titlebardrag", ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetFrameHeight()));
|
|
m_is_caption_hovered = ImGui::IsItemHovered();
|
|
ImGui::SetCursorPos(menu_min);
|
|
fileMenu();
|
|
editMenu();
|
|
entityMenu();
|
|
toolsMenu();
|
|
viewMenu();
|
|
|
|
float w = (ImGui::GetWindowContentRegionMax().x - ImGui::GetWindowContentRegionMin().x) * 0.5f - 30 - ImGui::GetCursorPosX();
|
|
ImGui::Dummy(ImVec2(w, ImGui::GetTextLineHeightWithSpacing()));
|
|
getAction("toggleGameMode")->toolbarButton(m_big_icon_font);
|
|
getAction("pauseGameMode")->toolbarButton(m_big_icon_font);
|
|
getAction("nextFrame")->toolbarButton(m_big_icon_font);
|
|
|
|
// we don't have custom titlebar on linux
|
|
#ifdef _WIN32
|
|
alignGUIRight([&](){
|
|
StaticString<200> stats;
|
|
if (m_engine->getFileSystem().hasWork()) stats.append(ICON_FA_HOURGLASS_HALF "Loading... | ");
|
|
stats.append("FPS: ", u32(m_fps + 0.5f));
|
|
if (m_frames_since_focused > 10) stats.append(" - inactive window");
|
|
ImGuiEx::TextUnformatted(stats);
|
|
|
|
if (m_log_ui->getUnreadErrorCount() == 1) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(1, 0, 0, 1), ICON_FA_EXCLAMATION_TRIANGLE "1 error | ");
|
|
}
|
|
else if (m_log_ui->getUnreadErrorCount() > 1)
|
|
{
|
|
StaticString<50> error_stats(ICON_FA_EXCLAMATION_TRIANGLE, m_log_ui->getUnreadErrorCount(), " errors | ");
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(1, 0, 0, 1), "%s", (const char*)error_stats);
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
if (ImGuiEx::IconButton(ICON_FA_WINDOW_MINIMIZE, nullptr)) os::minimizeWindow(m_engine->getWindowHandle());
|
|
ImGui::SameLine();
|
|
if (os::isMaximized(m_engine->getWindowHandle())) {
|
|
if (ImGuiEx::IconButton(ICON_FA_WINDOW_RESTORE, nullptr)) os::restore(m_engine->getWindowHandle());
|
|
}
|
|
else {
|
|
if (ImGuiEx::IconButton(ICON_FA_WINDOW_MAXIMIZE, nullptr)) os::maximizeWindow(m_engine->getWindowHandle());
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGuiEx::IconButton(ICON_FA_WINDOW_CLOSE, nullptr)) exit();
|
|
});
|
|
#endif
|
|
|
|
ImGui::EndMainMenuBar();
|
|
}
|
|
}
|
|
|
|
void getSelectionChain(Array<EntityRef>& chain, EntityPtr e) const {
|
|
if (!e.isValid()) return;
|
|
|
|
e = m_editor->getWorld()->getParent(*e);
|
|
while (e.isValid()) {
|
|
chain.push(*e);
|
|
e = m_editor->getWorld()->getParent(*e);
|
|
}
|
|
for (i32 i = 0; i < chain.size() / 2; ++i) {
|
|
swap(chain[i], chain[chain.size() - 1 - i]);
|
|
}
|
|
}
|
|
|
|
void showHierarchy(EntityRef entity, const Array<EntityRef>& selected_entities, Span<const EntityRef> selection_chain)
|
|
{
|
|
World* world = m_editor->getWorld();
|
|
bool is_selected = selected_entities.indexOf(entity) >= 0;
|
|
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_AllowItemOverlap;
|
|
bool has_child = world->getFirstChild(entity).isValid();
|
|
if (!has_child) flags = ImGuiTreeNodeFlags_Leaf;
|
|
if (is_selected) flags |= ImGuiTreeNodeFlags_Selected;
|
|
flags |= ImGuiTreeNodeFlags_SpanAvailWidth;
|
|
|
|
bool node_open;
|
|
if (m_renaming_entity == entity) {
|
|
node_open = ImGui::TreeNodeEx((void*)(intptr_t)entity.index, flags, "%s", "");
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(-1);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {0, 0});
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0);
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, 0);
|
|
if (m_set_rename_focus) {
|
|
ImGui::SetKeyboardFocusHere();
|
|
m_set_rename_focus = false;
|
|
}
|
|
if (ImGui::InputText("##renamed_val", m_rename_buf, sizeof(m_rename_buf), ImGuiInputTextFlags_EnterReturnsTrue)) {
|
|
m_editor->setEntityName((EntityRef)m_renaming_entity, m_rename_buf);
|
|
m_renaming_entity = INVALID_ENTITY;
|
|
}
|
|
if (ImGui::IsItemDeactivated() && m_renaming_entity.isValid()) {
|
|
if (ImGui::IsItemDeactivatedAfterEdit() && m_rename_buf[0]) {
|
|
m_editor->setEntityName((EntityRef)m_renaming_entity, m_rename_buf);
|
|
}
|
|
m_renaming_entity = INVALID_ENTITY;
|
|
}
|
|
m_set_rename_focus = false;
|
|
ImGui::PopStyleVar(2);
|
|
ImGui::PopStyleColor();
|
|
}
|
|
else {
|
|
const ImVec2 cp = ImGui::GetCursorPos();
|
|
ImGui::Dummy(ImVec2(1.f, ImGui::GetTextLineHeightWithSpacing()));
|
|
if (selection_chain.length() > 0 && selection_chain[0] == entity) {
|
|
ImGui::SetNextItemOpen(true);
|
|
selection_chain.removePrefix(1);
|
|
if (selection_chain.length() == 0) {
|
|
ImGui::SetScrollHereY();
|
|
}
|
|
}
|
|
if (ImGui::IsItemVisible()) {
|
|
ImGui::SetCursorPos(cp);
|
|
char buffer[1024];
|
|
getEntityListDisplayName(*this, *world, Span(buffer), entity);
|
|
node_open = ImGui::TreeNodeEx((void*)(intptr_t)entity.index, flags, "%s", buffer);
|
|
}
|
|
else {
|
|
const char* dummy = "";
|
|
const ImGuiID id = ImGui::GetCurrentWindow()->GetID((void*)(intptr_t)entity.index);
|
|
if (ImGui::TreeNodeUpdateNextOpen(id, flags)) {
|
|
ImGui::SetCursorPos(cp);
|
|
node_open = ImGui::TreeNodeBehavior(id, flags, dummy, dummy);
|
|
}
|
|
else {
|
|
node_open = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ImGui::IsItemVisible()) {
|
|
ImGui::PushID(entity.index);
|
|
if (ImGui::IsMouseReleased(1) && ImGui::IsItemHovered()) ImGui::OpenPopup("entity_context_menu");
|
|
if (ImGui::BeginPopup("entity_context_menu"))
|
|
{
|
|
if (ImGui::BeginMenu("Create child")) {
|
|
onCreateEntityWithComponentGUI(entity);
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
if (ImGui::MenuItem("Select all children")) {
|
|
Array<EntityRef> tmp(m_allocator);
|
|
for (EntityRef e : world->childrenOf(entity)) {
|
|
tmp.push(e);
|
|
}
|
|
m_editor->selectEntities(tmp, false);
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopID();
|
|
if (ImGui::BeginDragDropTarget()) {
|
|
if (auto* payload = ImGui::AcceptDragDropPayload("entity")) {
|
|
EntityRef dropped_entity = *(EntityRef*)payload->Data;
|
|
if (dropped_entity != entity) {
|
|
m_editor->makeParent(entity, dropped_entity);
|
|
ImGui::EndDragDropTarget();
|
|
if (node_open) ImGui::TreePop();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (auto* payload = ImGui::AcceptDragDropPayload("selected_entities")) {
|
|
const Array<EntityRef>& selected = m_editor->getSelectedEntities();
|
|
for (EntityRef e : selected) {
|
|
if (e != entity) {
|
|
m_editor->makeParent(entity, e);
|
|
}
|
|
}
|
|
ImGui::EndDragDropTarget();
|
|
if (node_open) ImGui::TreePop();
|
|
return;
|
|
}
|
|
|
|
ImGui::EndDragDropTarget();
|
|
}
|
|
|
|
if (ImGui::BeginDragDropSource())
|
|
{
|
|
char buffer[1024];
|
|
getEntityListDisplayName(*this, *world, Span(buffer), entity);
|
|
ImGui::TextUnformatted(buffer);
|
|
|
|
const Array<EntityRef>& selected = m_editor->getSelectedEntities();
|
|
if (selected.size() > 0 && selected.indexOf(entity) >= 0) {
|
|
ImGui::SetDragDropPayload("selected_entities", nullptr, 0);
|
|
}
|
|
else {
|
|
ImGui::SetDragDropPayload("entity", &entity, sizeof(entity));
|
|
}
|
|
ImGui::EndDragDropSource();
|
|
}
|
|
else {
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
|
m_editor->selectEntities(Span(&entity, 1), ImGui::GetIO().KeyCtrl);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (node_open)
|
|
{
|
|
for (EntityRef e : world->childrenOf(entity))
|
|
{
|
|
showHierarchy(e, selected_entities, selection_chain);
|
|
}
|
|
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows) && m_is_f2_pressed) {
|
|
m_renaming_entity = selected_entities.empty() ? INVALID_ENTITY : selected_entities[0];
|
|
if (m_renaming_entity.isValid()) {
|
|
m_set_rename_focus = true;
|
|
const char* name = m_editor->getWorld()->getEntityName(selected_entities[0]);
|
|
copyString(m_rename_buf, name);
|
|
}
|
|
}
|
|
|
|
ImGui::TreePop();
|
|
}
|
|
}
|
|
|
|
|
|
void folderUI(EntityFolders::FolderHandle folder_id, EntityFolders& folders, u32 level, Span<const EntityRef> selection_chain, const char* name_override, World::PartitionHandle partition) {
|
|
static EntityFolders::FolderHandle force_open_folder = EntityFolders::INVALID_FOLDER;
|
|
const EntityFolders::Folder* folder = &folders.getFolder(folder_id);
|
|
ImGui::PushID((const char*)&folder->id, (const char*)&folder->id + sizeof(folder->id));
|
|
bool node_open;
|
|
ImGuiTreeNodeFlags flags = level == 0 ? ImGuiTreeNodeFlags_DefaultOpen : 0;
|
|
flags |= ImGuiTreeNodeFlags_OpenOnArrow;
|
|
if (folders.getSelectedFolder() == folder_id) flags |= ImGuiTreeNodeFlags_Selected;
|
|
if (force_open_folder == folder_id) {
|
|
ImGui::SetNextItemOpen(true);
|
|
force_open_folder = EntityFolders::INVALID_FOLDER;
|
|
}
|
|
if (m_renaming_folder == folder_id) {
|
|
node_open = ImGui::TreeNodeEx((void*)folder, flags, "%s", ICON_FA_FOLDER);
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(-1);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {0, 0});
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0);
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, 0);
|
|
if (m_set_rename_focus) {
|
|
ImGui::SetKeyboardFocusHere();
|
|
m_set_rename_focus = false;
|
|
}
|
|
if (ImGui::InputText("##renamed_val", m_rename_buf, sizeof(m_rename_buf), ImGuiInputTextFlags_EnterReturnsTrue)) {
|
|
m_editor->renameEntityFolder(m_renaming_folder, m_rename_buf);
|
|
m_rename_buf[0] = 0;
|
|
}
|
|
if (ImGui::IsItemDeactivated()) {
|
|
if (ImGui::IsItemDeactivatedAfterEdit() && m_rename_buf[0]) {
|
|
m_editor->renameEntityFolder(m_renaming_folder, m_rename_buf);
|
|
}
|
|
m_renaming_folder = EntityFolders::INVALID_FOLDER;
|
|
}
|
|
m_set_rename_focus = false;
|
|
ImGui::PopStyleVar(2);
|
|
ImGui::PopStyleColor();
|
|
}
|
|
else {
|
|
if (name_override) {
|
|
node_open = ImGui::TreeNodeEx((void*)folder, flags, "%s%s", ICON_FA_HOME, name_override);
|
|
}
|
|
else {
|
|
node_open = ImGui::TreeNodeEx((void*)folder, flags, "%s%s", ICON_FA_FOLDER, folder->name);
|
|
}
|
|
}
|
|
|
|
if (ImGui::BeginDragDropTarget()) {
|
|
if (auto* payload = ImGui::AcceptDragDropPayload("entity")) {
|
|
EntityRef dropped_entity = *(EntityRef*)payload->Data;
|
|
m_editor->beginCommandGroup("move_entity_to_folder_group");
|
|
m_editor->makeParent(INVALID_ENTITY, dropped_entity);
|
|
m_editor->moveEntityToFolder(dropped_entity, folder_id);
|
|
m_editor->endCommandGroup();
|
|
}
|
|
ImGui::EndDragDropTarget();
|
|
}
|
|
|
|
if (ImGui::IsMouseClicked(0) && ImGui::IsItemHovered()) {
|
|
folders.selectFolder(folder_id);
|
|
}
|
|
|
|
if (ImGui::IsMouseReleased(1) && ImGui::IsItemHovered()) {
|
|
ImGui::OpenPopup("folder_context_menu");
|
|
}
|
|
if (ImGui::BeginPopup("folder_context_menu")) {
|
|
if (ImGui::Selectable("New folder")) {
|
|
force_open_folder = folder_id;
|
|
EntityFolders::FolderHandle new_folder = m_editor->createEntityFolder(folder_id);
|
|
folder = &folders.getFolder(folder_id);
|
|
m_renaming_folder = new_folder;
|
|
m_set_rename_focus = true;
|
|
}
|
|
const bool is_root = folder->parent == EntityFolders::INVALID_FOLDER;
|
|
World* world = m_editor->getWorld();
|
|
|
|
if (is_root) {
|
|
const bool is_partition_named = world->getPartition(partition).name[0];
|
|
if (is_partition_named) {
|
|
if (ImGui::Selectable("Save")) {
|
|
if (m_editor->isGameMode()) {
|
|
logError("Could not save while the game is running");
|
|
}
|
|
else {
|
|
m_editor->savePartition(partition);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if (ImGui::Selectable("Save As")) {
|
|
EntityFolders& folders = m_editor->getEntityFolders();
|
|
EntityFolders::FolderHandle root = folders.getRoot(partition);
|
|
folders.selectFolder(root);
|
|
saveAs();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!is_root || world->getPartitions().size() > 1) {
|
|
if (ImGui::Selectable(is_root ? "Unload" : "Delete")) {
|
|
if (is_root) {
|
|
m_confirm_destroy_partition = true;
|
|
m_partition_to_destroy = partition;
|
|
}
|
|
else {
|
|
m_editor->destroyEntityFolder(folder_id);
|
|
ImGui::EndPopup();
|
|
if (node_open) ImGui::TreePop();
|
|
ImGui::PopID();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const bool has_children = folders.getFolder(folder_id).first_entity.isValid();
|
|
if (ImGui::Selectable("Select entities", false, has_children ? 0 : ImGuiSelectableFlags_Disabled)) {
|
|
Array<EntityRef> entities(m_allocator);
|
|
EntityPtr e = folders.getFolder(folder_id).first_entity;
|
|
while (e.isValid()) {
|
|
entities.push((EntityRef)e);
|
|
const EntityPtr next = folders.getNextEntity((EntityRef)e);
|
|
e = next;
|
|
}
|
|
m_editor->selectEntities(entities, false);
|
|
}
|
|
if (level > 0 && ImGui::Selectable("Rename")) {
|
|
m_renaming_folder = folder_id;
|
|
m_set_rename_focus = true;
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
if (!node_open) {
|
|
ImGui::PopID();
|
|
return;
|
|
}
|
|
|
|
EntityFolders::FolderHandle child_id = folder->first_child;
|
|
while (child_id != EntityFolders::INVALID_FOLDER) {
|
|
const EntityFolders::Folder& child = folders.getFolder(child_id);
|
|
const EntityFolders::FolderHandle next = child.next;
|
|
folderUI(child_id, folders, level + 1, selection_chain, nullptr, partition);
|
|
child_id = next;
|
|
}
|
|
|
|
EntityPtr child_e = folder->first_entity;
|
|
while (child_e.isValid()) {
|
|
if (!m_editor->getWorld()->getParent((EntityRef)child_e).isValid()) {
|
|
showHierarchy((EntityRef)child_e, m_editor->getSelectedEntities(), selection_chain);
|
|
}
|
|
child_e = folders.getNextEntity((EntityRef)child_e);
|
|
}
|
|
|
|
ImGui::TreePop();
|
|
ImGui::PopID();
|
|
}
|
|
|
|
void guiEntityList() {
|
|
PROFILE_FUNCTION();
|
|
const Array<EntityRef>& entities = m_editor->getSelectedEntities();
|
|
static TextFilter filter;
|
|
if (!m_is_entity_list_open) return;
|
|
if (ImGui::Begin(ICON_FA_STREAM "Hierarchy##hierarchy", &m_is_entity_list_open))
|
|
{
|
|
World* world = m_editor->getWorld();
|
|
filter.gui(ICON_FA_SEARCH "Filter");
|
|
|
|
if (ImGui::BeginChild("entities")) {
|
|
ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x);
|
|
|
|
if (filter.isActive()) {
|
|
for (EntityPtr e = world->getFirstEntity(); e.isValid(); e = world->getNextEntity((EntityRef)e)) {
|
|
char buffer[1024];
|
|
getEntityListDisplayName(*this, *world, Span(buffer), e);
|
|
if (!filter.pass(buffer)) continue;
|
|
|
|
ImGui::PushID(e.index);
|
|
const EntityRef e_ref = (EntityRef)e;
|
|
bool selected = entities.indexOf(e_ref) >= 0;
|
|
if (ImGui::Selectable(buffer, &selected, ImGuiSelectableFlags_SpanAvailWidth)) {
|
|
m_editor->selectEntities(Span(&e_ref, 1), ImGui::GetIO().KeyCtrl);
|
|
}
|
|
if (ImGui::BeginDragDropSource()) {
|
|
ImGui::TextUnformatted(buffer);
|
|
ImGui::SetDragDropPayload("entity", &e, sizeof(e));
|
|
ImGui::EndDragDropSource();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
} else {
|
|
EntityFolders& folders = m_editor->getEntityFolders();
|
|
Array<EntityRef> selection_chain(m_allocator);
|
|
if (m_entity_selection_changed && !m_editor->getSelectedEntities().empty()) {
|
|
getSelectionChain(selection_chain, m_editor->getSelectedEntities()[0]);
|
|
m_entity_selection_changed = false;
|
|
}
|
|
for (const World::Partition& p : world->getPartitions()) {
|
|
folderUI(folders.getRoot(p.handle), folders, 0, selection_chain, p.name, p.handle);
|
|
}
|
|
}
|
|
ImGui::PopItemWidth();
|
|
}
|
|
ImGui::EndChild();
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
void setFullscreen(bool fullscreen) override
|
|
{
|
|
if (fullscreen) {
|
|
m_fullscreen_restore_state = os::setFullscreen(m_main_window);
|
|
}
|
|
else {
|
|
os::restore(m_main_window, m_fullscreen_restore_state);
|
|
}
|
|
}
|
|
|
|
void saveSettings() override {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
if (io.WantSaveIniSettings) {
|
|
const char* data = ImGui::SaveIniSettingsToMemory();
|
|
m_settings.m_imgui_state = data;
|
|
io.WantSaveIniSettings = false;
|
|
}
|
|
m_settings.m_is_entity_list_open = m_is_entity_list_open;
|
|
m_settings.setValue(Settings::LOCAL, "fileselector_dir", m_file_selector.m_current_dir.c_str());
|
|
|
|
m_settings.m_is_maximized = os::isMaximized(m_main_window);
|
|
if (!os::isMinimized(m_main_window)) {
|
|
os::Rect win_rect = os::getWindowScreenRect(m_main_window);
|
|
m_settings.m_window.x = win_rect.left;
|
|
m_settings.m_window.y = win_rect.top;
|
|
m_settings.m_window.w = win_rect.width;
|
|
m_settings.m_window.h = win_rect.height;
|
|
}
|
|
|
|
for (auto* i : m_gui_plugins) {
|
|
i->onBeforeSettingsSaved();
|
|
}
|
|
|
|
if (m_settings.save()) {
|
|
logInfo("Settings saved");
|
|
}
|
|
else {
|
|
logError("Settings failed to save");
|
|
}
|
|
}
|
|
|
|
ImFont* addFontFromFile(const char* path, float size, bool merge_icons) {
|
|
PROFILE_FUNCTION();
|
|
FileSystem& fs = m_engine->getFileSystem();
|
|
OutputMemoryStream data(m_allocator);
|
|
if (!fs.getContentSync(Path(path), data)) return nullptr;
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
ImFontConfig cfg;
|
|
copyString(cfg.Name, path);
|
|
cfg.FontDataOwnedByAtlas = false;
|
|
auto font = io.Fonts->AddFontFromMemoryTTF((void*)data.data(), (i32)data.size(), size, &cfg);
|
|
if (merge_icons) {
|
|
ImFontConfig config;
|
|
copyString(config.Name, "editor/fonts/fa-regular-400.ttf");
|
|
config.MergeMode = true;
|
|
config.FontDataOwnedByAtlas = false;
|
|
config.GlyphMinAdvanceX = size; // Use if you want to make the icon monospaced
|
|
static const ImWchar icon_ranges[] = { ICON_MIN_FA, ICON_MAX_FA, 0 };
|
|
OutputMemoryStream icons_data(m_allocator);
|
|
if (fs.getContentSync(Path("editor/fonts/fa-regular-400.ttf"), icons_data)) {
|
|
ImFont* icons_font = io.Fonts->AddFontFromMemoryTTF((void*)icons_data.data(), (i32)icons_data.size(), size * 0.75f, &config, icon_ranges);
|
|
ASSERT(icons_font);
|
|
}
|
|
copyString(config.Name, "editor/fonts/fa-solid-900.ttf");
|
|
icons_data.clear();
|
|
if (fs.getContentSync(Path("editor/fonts/fa-solid-900.ttf"), icons_data)) {
|
|
ImFont* icons_font = io.Fonts->AddFontFromMemoryTTF((void*)icons_data.data(), (i32)icons_data.size(), size * 0.75f, &config, icon_ranges);
|
|
ASSERT(icons_font);
|
|
}
|
|
}
|
|
|
|
return font;
|
|
}
|
|
|
|
void initIMGUIPlatformIO() {
|
|
ImGuiPlatformIO& pio = ImGui::GetPlatformIO();
|
|
static StudioAppImpl* that = this;
|
|
ASSERT(that == this);
|
|
pio.Platform_CreateWindow = [](ImGuiViewport* vp){
|
|
os::InitWindowArgs args = {};
|
|
args.flags = os::InitWindowArgs::NO_DECORATION | os::InitWindowArgs::NO_TASKBAR_ICON;
|
|
ImGuiViewport* parent = ImGui::FindViewportByID(vp->ParentViewportId);
|
|
args.parent = parent ? parent->PlatformHandle : os::INVALID_WINDOW;
|
|
args.name = "child";
|
|
//args.hit_test_callback = &StudioAppImpl::childHitTestCallback;
|
|
vp->PlatformHandle = os::createWindow(args);
|
|
that->m_windows.push(vp->PlatformHandle);
|
|
|
|
ASSERT(vp->PlatformHandle != os::INVALID_WINDOW);
|
|
};
|
|
pio.Platform_DestroyWindow = [](ImGuiViewport* vp){
|
|
os::WindowHandle w = (os::WindowHandle)vp->PlatformHandle;
|
|
that->m_deferred_destroy_windows.push({w, 4});
|
|
vp->PlatformHandle = nullptr;
|
|
vp->PlatformUserData = nullptr;
|
|
that->m_windows.eraseItem(w);
|
|
};
|
|
pio.Platform_ShowWindow = [](ImGuiViewport* vp){};
|
|
pio.Platform_SetWindowPos = [](ImGuiViewport* vp, ImVec2 pos) {
|
|
const os::WindowHandle h = vp->PlatformHandle;
|
|
os::Rect r = os::getWindowScreenRect(h);
|
|
r.left = int(pos.x);
|
|
r.top = int(pos.y);
|
|
os::setWindowScreenRect(h, r);
|
|
};
|
|
pio.Platform_GetWindowPos = [](ImGuiViewport* vp) -> ImVec2 {
|
|
os::WindowHandle win = (os::WindowHandle)vp->PlatformHandle;
|
|
const os::Rect r = os::getWindowClientRect(win);
|
|
const os::Point p = os::toScreen(win, r.left, r.top);
|
|
return {(float)p.x, (float)p.y};
|
|
|
|
};
|
|
pio.Platform_SetWindowSize = [](ImGuiViewport* vp, ImVec2 pos) {
|
|
const os::WindowHandle h = vp->PlatformHandle;
|
|
os::Rect r = os::getWindowScreenRect(h);
|
|
r.width = int(pos.x);
|
|
r.height = int(pos.y);
|
|
os::setWindowScreenRect(h, r);
|
|
};
|
|
pio.Platform_GetWindowSize = [](ImGuiViewport* vp) -> ImVec2 {
|
|
const os::Rect r = os::getWindowClientRect((os::WindowHandle)vp->PlatformHandle);
|
|
return {(float)r.width, (float)r.height};
|
|
};
|
|
pio.Platform_SetWindowTitle = [](ImGuiViewport* vp, const char* title){
|
|
os::setWindowTitle((os::WindowHandle)vp->PlatformHandle, title);
|
|
};
|
|
pio.Platform_GetWindowFocus = [](ImGuiViewport* vp){
|
|
return os::getFocused() == vp->PlatformHandle;
|
|
};
|
|
pio.Platform_SetWindowFocus = nullptr;
|
|
|
|
ImGuiViewport* mvp = ImGui::GetMainViewport();
|
|
mvp->PlatformHandle = m_main_window;
|
|
mvp->DpiScale = 1;
|
|
mvp->PlatformUserData = (void*)1;
|
|
|
|
updateIMGUIMonitors();
|
|
}
|
|
|
|
static void updateIMGUIMonitors() {
|
|
os::Monitor monitors[32];
|
|
const u32 monitor_count = os::getMonitors(Span(monitors));
|
|
ImGuiPlatformIO& pio = ImGui::GetPlatformIO();
|
|
pio.Monitors.resize(0);
|
|
for (u32 i = 0; i < monitor_count; ++i) {
|
|
const os::Monitor& m = monitors[i];
|
|
ImGuiPlatformMonitor im;
|
|
im.MainPos = ImVec2((float)m.monitor_rect.left, (float)m.monitor_rect.top);
|
|
im.MainSize = ImVec2((float)m.monitor_rect.width, (float)m.monitor_rect.height);
|
|
im.WorkPos = ImVec2((float)m.work_rect.left, (float)m.work_rect.top);
|
|
im.WorkSize = ImVec2((float)m.work_rect.width, (float)m.work_rect.height);
|
|
|
|
if (m.primary) {
|
|
pio.Monitors.push_front(im);
|
|
}
|
|
else {
|
|
pio.Monitors.push_back(im);
|
|
}
|
|
}
|
|
}
|
|
|
|
void beginInitIMGUI() {
|
|
PROFILE_FUNCTION();
|
|
ImGui::SetAllocatorFunctions(imguiAlloc, imguiFree, this);
|
|
ImGui::CreateContext();
|
|
loadSettings(); // needs imgui context
|
|
|
|
jobs::runLambda([this](){
|
|
PROFILE_BLOCK("init imgui");
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.IniFilename = nullptr;
|
|
#ifdef __linux__
|
|
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
|
io.BackendFlags = ImGuiBackendFlags_HasMouseCursors;
|
|
#else
|
|
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable | ImGuiConfigFlags_ViewportsEnable;
|
|
io.BackendFlags = ImGuiBackendFlags_PlatformHasViewports | ImGuiBackendFlags_RendererHasViewports | ImGuiBackendFlags_HasMouseCursors;
|
|
#endif
|
|
|
|
initIMGUIPlatformIO();
|
|
|
|
const int dpi = os::getDPI();
|
|
float font_scale = dpi / 96.f;
|
|
FileSystem& fs = m_engine->getFileSystem();
|
|
|
|
ImGui::LoadIniSettingsFromMemory(m_settings.m_imgui_state.c_str());
|
|
|
|
m_font = addFontFromFile("editor/fonts/notosans-regular.ttf", (float)m_settings.m_font_size * font_scale, true);
|
|
m_bold_font = addFontFromFile("editor/fonts/notosans-bold.ttf", (float)m_settings.m_font_size * font_scale, true);
|
|
m_monospace_font = addFontFromFile("editor/fonts/sourcecodepro-regular.ttf", (float)m_settings.m_font_size * font_scale, false);
|
|
|
|
OutputMemoryStream data(m_allocator);
|
|
if (fs.getContentSync(Path("editor/fonts/fa-solid-900.ttf"), data)) {
|
|
const float size = (float)m_settings.m_font_size * font_scale * 1.25f;
|
|
ImFontConfig cfg;
|
|
copyString(cfg.Name, "editor/fonts/fa-solid-900.ttf");
|
|
cfg.FontDataOwnedByAtlas = false;
|
|
cfg.GlyphMinAdvanceX = size; // Use if you want to make the icon monospaced
|
|
static const ImWchar icon_ranges[] = { ICON_MIN_FA, ICON_MAX_FA, 0 };
|
|
m_big_icon_font = io.Fonts->AddFontFromMemoryTTF((void*)data.data(), (i32)data.size(), size, &cfg, icon_ranges);
|
|
cfg.MergeMode = true;
|
|
copyString(cfg.Name, "editor/fonts/fa-regular-400.ttf");
|
|
if (fs.getContentSync(Path("editor/fonts/fa-regular-400.ttf"), data)) {
|
|
ImFont* icons_font = io.Fonts->AddFontFromMemoryTTF((void*)data.data(), (i32)data.size(), size, &cfg, icon_ranges);
|
|
ASSERT(icons_font);
|
|
}
|
|
}
|
|
|
|
if (!m_font || !m_bold_font) {
|
|
os::messageBox(
|
|
"Could not open editor/fonts/notosans-regular.ttf or editor/fonts/NotoSans-Bold.ttf\n"
|
|
"It very likely means that data are not bundled with\n"
|
|
"the exe and the exe is not in the correct directory.\n"
|
|
"The program will eventually crash!"
|
|
);
|
|
}
|
|
if (!m_monospace_font) logError("Failed to load monospace font");
|
|
|
|
{
|
|
PROFILE_BLOCK("build atlas");
|
|
ImFontAtlas* atlas = ImGui::GetIO().Fonts;
|
|
atlas->FontBuilderIO = ImGuiFreeType::GetBuilderForFreeType();
|
|
atlas->FontBuilderFlags = 0;
|
|
atlas->Build();
|
|
}
|
|
|
|
ImGuiStyle& style = ImGui::GetStyle();
|
|
style.FramePadding.y = 0;
|
|
style.ItemSpacing.y = 2;
|
|
style.ItemInnerSpacing.x = 2;
|
|
}, &m_init_imgui_signal);
|
|
}
|
|
|
|
void setRenderInterface(RenderInterface* ri) override { m_render_interface = ri; }
|
|
RenderInterface* getRenderInterface() override { return m_render_interface; }
|
|
|
|
float getFOV() const override { return m_fov; }
|
|
void setFOV(float fov_radians) override { m_fov = fov_radians; }
|
|
Settings& getSettings() override { return m_settings; }
|
|
|
|
void loadSettings() {
|
|
PROFILE_FUNCTION();
|
|
logInfo("Loading settings...");
|
|
char cmd_line[2048];
|
|
os::getCommandLine(Span(cmd_line));
|
|
|
|
CommandLineParser parser(cmd_line);
|
|
while (parser.next())
|
|
{
|
|
if (!parser.currentEquals("-no_crash_report")) continue;
|
|
|
|
m_settings.m_force_no_crash_report = true;
|
|
break;
|
|
}
|
|
|
|
m_settings.load();
|
|
for (auto* i : m_gui_plugins) {
|
|
i->onSettingsLoaded();
|
|
}
|
|
|
|
m_is_entity_list_open = m_settings.m_is_entity_list_open;
|
|
|
|
if (m_settings.m_is_maximized)
|
|
{
|
|
os::maximizeWindow(m_main_window);
|
|
}
|
|
else if (m_settings.m_window.w > 0)
|
|
{
|
|
os::Rect r;
|
|
r.left = m_settings.m_window.x;
|
|
r.top = m_settings.m_window.y;
|
|
r.width = m_settings.m_window.w;
|
|
r.height = m_settings.m_window.h;
|
|
os::setWindowScreenRect(m_main_window, r);
|
|
}
|
|
m_export.dest_dir = "";
|
|
m_settings.getValue(Settings::LOCAL, "export_dir", Span(m_export.dest_dir.data));
|
|
m_settings.getValue(Settings::LOCAL, "export_pack", m_export.pack);
|
|
m_file_selector.m_current_dir = m_settings.getStringValue(Settings::LOCAL, "fileselector_dir", "");
|
|
}
|
|
|
|
CommonActions& getCommonActions() override { return m_common_actions; }
|
|
|
|
void showAllActionsGUI() { m_show_all_actions_request = true; }
|
|
|
|
void addActions()
|
|
{
|
|
m_common_actions.save.init("Save", "Save", "save", ICON_FA_SAVE, os::Keycode::S, Action::Modifiers::CTRL, Action::GLOBAL);
|
|
m_common_actions.save.func.bind<&StudioAppImpl::save>(this);
|
|
addAction(&m_common_actions.save);
|
|
m_common_actions.undo.init("Undo", "Undo", "undo", ICON_FA_UNDO, os::Keycode::Z, Action::Modifiers::CTRL, Action::IMGUI_PRIORITY);
|
|
m_common_actions.undo.func.bind<&StudioAppImpl::undo>(this);
|
|
addAction(&m_common_actions.undo);
|
|
m_common_actions.redo.init("Redo", "Redo", "redo", ICON_FA_REDO, os::Keycode::Z, Action::Modifiers::CTRL | Action::Modifiers::SHIFT, Action::IMGUI_PRIORITY);
|
|
m_common_actions.redo.func.bind<&StudioAppImpl::redo>(this);
|
|
addAction(&m_common_actions.redo);
|
|
m_common_actions.del.init("Delete", "Delete", "delete", ICON_FA_MINUS_SQUARE, os::Keycode::DEL, Action::Modifiers::NONE, Action::IMGUI_PRIORITY);
|
|
m_common_actions.del.func.bind<&StudioAppImpl::destroySelectedEntity>(this);
|
|
addAction(&m_common_actions.del);
|
|
|
|
m_common_actions.cam_orbit.init("Orbit", "Orbit with RMB", "orbitRMB", "", Action::LOCAL);
|
|
addAction(&m_common_actions.cam_orbit);
|
|
m_common_actions.cam_forward.init("Move forward", "Move camera forward", "moveForward", "", Action::LOCAL);
|
|
addAction(&m_common_actions.cam_forward);
|
|
m_common_actions.cam_backward.init("Move back", "Move camera back", "moveBack", "", Action::LOCAL);
|
|
addAction(&m_common_actions.cam_backward);
|
|
m_common_actions.cam_left.init("Move left", "Move camera left", "moveLeft", "", Action::LOCAL);
|
|
addAction(&m_common_actions.cam_left);
|
|
m_common_actions.cam_right.init("Move right", "Move camera right", "moveRight", "", Action::LOCAL);
|
|
addAction(&m_common_actions.cam_right);
|
|
m_common_actions.cam_up.init("Move up", "Move camera up", "moveUp", "", Action::LOCAL);
|
|
addAction(&m_common_actions.cam_up);
|
|
m_common_actions.cam_down.init("Move down", "Move camera down", "moveDown", "", Action::LOCAL);
|
|
addAction(&m_common_actions.cam_down);
|
|
|
|
m_show_all_actions_action.init("Show all actions", "Show all actions", "show_all_actions", "", os::Keycode::P, Action::Modifiers::CTRL | Action::Modifiers::SHIFT, Action::Type::IMGUI_PRIORITY);
|
|
m_show_all_actions_action.func.bind<&StudioAppImpl::showAllActionsGUI>(this);
|
|
addAction(&m_show_all_actions_action);
|
|
|
|
addAction<&StudioAppImpl::newWorld>("New", "New world", "newWorld", ICON_FA_PLUS);
|
|
addAction<&StudioAppImpl::saveAs>("Save As", "Save world as", "saveAs", "", os::Keycode::S, Action::Modifiers::CTRL | Action::Modifiers::SHIFT);
|
|
addAction<&StudioAppImpl::exit>("Exit", "Exit Studio", "exit", ICON_FA_SIGN_OUT_ALT);
|
|
addAction<&StudioAppImpl::copy>("Copy", "Copy entity", "copy", ICON_FA_CLIPBOARD, os::Keycode::C, Action::Modifiers::CTRL);
|
|
addAction<&StudioAppImpl::paste>("Paste", "Paste entity", "paste", ICON_FA_PASTE, os::Keycode::V, Action::Modifiers::CTRL);
|
|
addAction<&StudioAppImpl::duplicate>("Duplicate", "Duplicate entity", "duplicate", ICON_FA_CLONE, os::Keycode::D, Action::Modifiers::CTRL);
|
|
addAction<&StudioAppImpl::setTranslateGizmoMode>("Translate", "Set translate mode", "setTranslateGizmoMode", ICON_FA_ARROWS_ALT)
|
|
.is_selected.bind<&Gizmo::Config::isTranslateMode>(&getGizmoConfig());
|
|
addAction<&StudioAppImpl::setRotateGizmoMode>("Rotate", "Set rotate mode", "setRotateGizmoMode", ICON_FA_UNDO)
|
|
.is_selected.bind<&Gizmo::Config::isRotateMode>(&getGizmoConfig());
|
|
addAction<&StudioAppImpl::setScaleGizmoMode>("Scale", "Set scale mode", "setScaleGizmoMode", ICON_FA_EXPAND_ALT)
|
|
.is_selected.bind<&Gizmo::Config::isScaleMode>(&getGizmoConfig());
|
|
addAction<&StudioAppImpl::setLocalCoordSystem>("Local", "Set local transform system", "setLocalCoordSystem", ICON_FA_HOME)
|
|
.is_selected.bind<&Gizmo::Config::isLocalCoordSystem>(&getGizmoConfig());
|
|
addAction<&StudioAppImpl::setGlobalCoordSystem>("Global", "Set global transform system", "setGlobalCoordSystem", ICON_FA_GLOBE)
|
|
.is_selected.bind<&Gizmo::Config::isGlobalCoordSystem>(&getGizmoConfig());
|
|
|
|
addAction<&StudioAppImpl::addEntity>("Create empty", "Create empty entity", "createEntity", ICON_FA_PLUS_SQUARE);
|
|
|
|
addAction<&StudioAppImpl::makeParent>("Make parent", "Make entity parent", "makeParent", ICON_FA_OBJECT_GROUP);
|
|
addAction<&StudioAppImpl::unparent>("Unparent", "Unparent entity", "unparent", ICON_FA_OBJECT_UNGROUP);
|
|
|
|
addAction<&StudioAppImpl::nextFrame>("Next frame", "Next frame", "nextFrame", ICON_FA_STEP_FORWARD);
|
|
addAction<&StudioAppImpl::pauseGame>("Pause", "Pause game mode", "pauseGameMode", ICON_FA_PAUSE)
|
|
.is_selected.bind<&Engine::isPaused>(m_engine.get());
|
|
addAction<&StudioAppImpl::toggleGameMode>("Game Mode", "Toggle game mode", "toggleGameMode", ICON_FA_PLAY)
|
|
.is_selected.bind<&WorldEditor::isGameMode>(m_editor.get());
|
|
addAction<&StudioAppImpl::autosnapDown>("Autosnap down", "Toggle autosnap down", "autosnapDown")
|
|
.is_selected.bind<&Gizmo::Config::isAutosnapDown>(&getGizmoConfig());
|
|
addAction<&StudioAppImpl::snapDown>("Snap down", "Snap entities down", "snapDown");
|
|
addAction<&StudioAppImpl::toggleEntityList>("Hierarchy", "Toggle hierarchy", "entityList", ICON_FA_STREAM)
|
|
.is_selected.bind<&StudioAppImpl::isEntityListOpen>(this);
|
|
addAction<&StudioAppImpl::toggleSettings>("Settings", "Toggle settings UI", "settings", ICON_FA_COG)
|
|
.is_selected.bind<&StudioAppImpl::areSettingsOpen>(this);
|
|
addAction<&StudioAppImpl::showExportGameDialog>("Export game", "Export game", "export_game", ICON_FA_FILE_EXPORT);
|
|
}
|
|
|
|
|
|
static bool copyPlugin(const char* src, int iteration, char (&out)[MAX_PATH])
|
|
{
|
|
char tmp_path[MAX_PATH];
|
|
os::getExecutablePath(Span(tmp_path));
|
|
StaticString<MAX_PATH> copy_path(Path::getDir(tmp_path), "plugins/", iteration);
|
|
if (!os::makePath(copy_path)) logError("Could not create ", copy_path);
|
|
copyString(Span(tmp_path), Path::getBasename(src));
|
|
copy_path.append("/", tmp_path, ".", getPluginExtension());
|
|
#ifdef _WIN32
|
|
StaticString<MAX_PATH> src_pdb(src);
|
|
StaticString<MAX_PATH> dest_pdb(copy_path);
|
|
if (Path::replaceExtension(dest_pdb.data, "pdb") && Path::replaceExtension(src_pdb.data, "pda"))
|
|
{
|
|
os::deleteFile(dest_pdb);
|
|
if (!os::copyFile(src_pdb, dest_pdb))
|
|
{
|
|
copyString(out, src);
|
|
return false;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
os::deleteFile(copy_path);
|
|
if (!os::copyFile(src, copy_path))
|
|
{
|
|
copyString(out, src);
|
|
return false;
|
|
}
|
|
copyString(out, copy_path);
|
|
return true;
|
|
}
|
|
|
|
|
|
void loadUserPlugins() {
|
|
PROFILE_FUNCTION();
|
|
char cmd_line[2048];
|
|
os::getCommandLine(Span(cmd_line));
|
|
|
|
CommandLineParser parser(cmd_line);
|
|
SystemManager& system_manager = m_engine->getSystemManager();
|
|
while (parser.next())
|
|
{
|
|
if (!parser.currentEquals("-plugin")) continue;
|
|
if (!parser.next()) break;
|
|
|
|
char src[MAX_PATH];
|
|
parser.getCurrent(src, lengthOf(src));
|
|
|
|
bool is_full_path = contains(src, '.');
|
|
Lumix::ISystem* loaded_plugin;
|
|
if (is_full_path)
|
|
{
|
|
char copy_path[MAX_PATH];
|
|
copyPlugin(src, 0, copy_path);
|
|
loaded_plugin = system_manager.load(copy_path);
|
|
}
|
|
else
|
|
{
|
|
loaded_plugin = system_manager.load(src);
|
|
}
|
|
|
|
if (!loaded_plugin)
|
|
{
|
|
logError("Could not load plugin ", src, " requested by command line");
|
|
}
|
|
else if (is_full_path && !m_watched_plugin.watcher.get())
|
|
{
|
|
char dir[MAX_PATH];
|
|
copyString(Span(m_watched_plugin.basename.data), Path::getBasename(src));
|
|
copyString(Span(dir), Path::getDir(src));
|
|
m_watched_plugin.watcher = FileSystemWatcher::create(dir, m_allocator);
|
|
m_watched_plugin.watcher->getCallback().bind<&StudioAppImpl::onPluginChanged>(this);
|
|
m_watched_plugin.dir = dir;
|
|
m_watched_plugin.system = loaded_plugin;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static const char* getPluginExtension()
|
|
{
|
|
const char* ext =
|
|
#ifdef _WIN32
|
|
"dll";
|
|
#elif defined __linux__
|
|
"so";
|
|
#else
|
|
#error Unknown platform
|
|
#endif
|
|
return ext;
|
|
}
|
|
|
|
|
|
void onPluginChanged(const char* path)
|
|
{
|
|
const char* ext = getPluginExtension();
|
|
if (!Path::hasExtension(path, ext)
|
|
#ifdef _WIN32
|
|
&& !Path::hasExtension(path, "pda")
|
|
#endif
|
|
)
|
|
return;
|
|
|
|
if (!equalIStrings(Path::getBasename(path), m_watched_plugin.basename)) return;
|
|
|
|
m_watched_plugin.reload_request = true;
|
|
}
|
|
|
|
|
|
void tryReloadPlugin() {
|
|
m_watched_plugin.reload_request = false;
|
|
|
|
StaticString<MAX_PATH> src(m_watched_plugin.dir, m_watched_plugin.basename, ".", getPluginExtension());
|
|
char copy_path[MAX_PATH];
|
|
++m_watched_plugin.iteration;
|
|
|
|
if (!copyPlugin(src, m_watched_plugin.iteration, copy_path)) return;
|
|
|
|
logInfo("Trying to reload plugin ", m_watched_plugin.basename);
|
|
|
|
OutputMemoryStream blob(m_allocator);
|
|
blob.reserve(16 * 1024);
|
|
SystemManager& system_manager = m_engine->getSystemManager();
|
|
|
|
World* world = m_editor->getWorld();
|
|
auto& modules = world->getModules();
|
|
for (i32 i = 0, c = modules.size(); i < c; ++i) {
|
|
UniquePtr<IModule>& module = modules[i];
|
|
if (&module->getSystem() != m_watched_plugin.system) continue;
|
|
|
|
module->beforeReload(blob);
|
|
modules.erase(i);
|
|
break;
|
|
}
|
|
system_manager.unload(m_watched_plugin.system);
|
|
|
|
// TODO try to delete the old version
|
|
|
|
m_watched_plugin.system = system_manager.load(copy_path);
|
|
if (!m_watched_plugin.system) {
|
|
logError("Failed to load plugin ", copy_path, ". Reload failed.");
|
|
return;
|
|
}
|
|
|
|
InputMemoryStream input_blob(blob);
|
|
m_watched_plugin.system->createModules(*world);
|
|
for (const UniquePtr<IModule>& module : world->getModules()) {
|
|
if (&module->getSystem() != m_watched_plugin.system) continue;
|
|
module->afterReload(input_blob);
|
|
}
|
|
logInfo("Finished reloading plugin.");
|
|
}
|
|
|
|
bool workersCountOption(u32& workers_count) {
|
|
char cmd_line[2048];
|
|
os::getCommandLine(Span(cmd_line));
|
|
|
|
CommandLineParser parser(cmd_line);
|
|
while (parser.next())
|
|
{
|
|
if (parser.currentEquals("-workers")) {
|
|
if(!parser.next()) {
|
|
logError("command line option '-workers` without value");
|
|
return false;
|
|
}
|
|
char tmp[64];
|
|
parser.getCurrent(tmp, sizeof(tmp));
|
|
fromCString(tmp, workers_count);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
|
|
}
|
|
|
|
void loadWorldFromCommandLine()
|
|
{
|
|
char cmd_line[2048];
|
|
char path[MAX_PATH];
|
|
os::getCommandLine(Span(cmd_line));
|
|
|
|
CommandLineParser parser(cmd_line);
|
|
while (parser.next())
|
|
{
|
|
if (!parser.currentEquals("-open")) continue;
|
|
if (!parser.next()) break;
|
|
|
|
parser.getCurrent(path, lengthOf(path));
|
|
loadWorld(Path(path), false);
|
|
m_is_welcome_screen_open = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void checkDataDirCommandLine(char* dir, int max_size)
|
|
{
|
|
char cmd_line[2048];
|
|
os::getCommandLine(Span(cmd_line));
|
|
|
|
CommandLineParser parser(cmd_line);
|
|
while (parser.next())
|
|
{
|
|
if (!parser.currentEquals("-data_dir")) continue;
|
|
if (!parser.next()) break;
|
|
|
|
parser.getCurrent(dir, max_size);
|
|
break;
|
|
}
|
|
}
|
|
|
|
Span<MousePlugin*> getMousePlugins() override { return m_mouse_plugins; }
|
|
|
|
MousePlugin* getMousePlugin(const char* name) override {
|
|
for (auto* i : m_mouse_plugins) {
|
|
if (equalStrings(i->getName(), name)) return i;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
IPlugin* getIPlugin(const char* name) override {
|
|
for (auto* i : m_plugins) {
|
|
if (equalStrings(i->getName(), name)) return i;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
GUIPlugin* getGUIPlugin(const char* name) override {
|
|
for (auto* i : m_gui_plugins) {
|
|
if (equalStrings(i->getName(), name)) return i;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
void initPlugins() {
|
|
PROFILE_FUNCTION();
|
|
#ifdef STATIC_PLUGINS
|
|
#define LUMIX_EDITOR_PLUGINS
|
|
#include "engine/plugins.inl"
|
|
#undef LUMIX_EDITOR_PLUGINS
|
|
#else
|
|
auto& plugin_manager = m_engine->getSystemManager();
|
|
for (auto* lib : plugin_manager.getLibraries())
|
|
{
|
|
auto* f = (StudioApp::IPlugin * (*)(StudioApp&)) os::getLibrarySymbol(lib, "setStudioApp");
|
|
if (f)
|
|
{
|
|
StudioApp::IPlugin* plugin = f(*this);
|
|
if (plugin) addPlugin(*plugin);
|
|
}
|
|
}
|
|
#endif
|
|
addPlugin(*createSplineEditor(*this));
|
|
addPlugin(*m_property_grid.get());
|
|
addPlugin(*m_log_ui.get());
|
|
addPlugin(*m_asset_browser.get());
|
|
addPlugin(*m_profiler_ui.get());
|
|
|
|
for (IPlugin* plugin : m_plugins) {
|
|
logInfo("Studio plugin ", plugin->getName(), " loaded");
|
|
}
|
|
|
|
for (int i = 1, c = m_plugins.size(); i < c; ++i) {
|
|
for (int j = 0; j < i; ++j) {
|
|
IPlugin* p = m_plugins[i];
|
|
if (m_plugins[j]->dependsOn(*p)) {
|
|
m_plugins.erase(i);
|
|
--i;
|
|
m_plugins.insert(j, p);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (IPlugin* plugin : m_plugins) {
|
|
plugin->init();
|
|
}
|
|
|
|
for (const reflection::RegisteredComponent& cmp : reflection::getComponents()) {
|
|
ASSERT(cmp.cmp->component_type != INVALID_COMPONENT_TYPE);
|
|
const reflection::ComponentBase* r = cmp.cmp;
|
|
|
|
if (m_component_labels.find(r->component_type).isValid()) continue;
|
|
|
|
struct : reflection::IEmptyPropertyVisitor {
|
|
void visit(const reflection::Property<Path>& prop) override {
|
|
for (const reflection::IAttribute* attr : prop.attributes) {
|
|
if (attr->getType() == reflection::IAttribute::RESOURCE) {
|
|
is_res = true;
|
|
reflection::ResourceAttribute* a = (reflection::ResourceAttribute*)attr;
|
|
res_type = a->resource_type;
|
|
prop_name = prop.name;
|
|
}
|
|
}
|
|
}
|
|
bool is_res = false;
|
|
const char* prop_name;
|
|
ResourceType res_type;
|
|
} visitor;
|
|
|
|
r->visit(visitor);
|
|
if (visitor.is_res) {
|
|
registerComponent(r->icon, r->component_type, r->label, visitor.res_type, visitor.prop_name);
|
|
}
|
|
else {
|
|
registerComponent(r->icon, r->component_type, r->label);
|
|
}
|
|
}
|
|
PrefabSystem::createEditorPlugins(*this, m_editor->getPrefabSystem());
|
|
}
|
|
|
|
|
|
void addPlugin(IPlugin& plugin) override { m_plugins.push(&plugin); }
|
|
|
|
|
|
void addPlugin(GUIPlugin& plugin) override
|
|
{
|
|
m_gui_plugins.push(&plugin);
|
|
for (auto* i : m_gui_plugins)
|
|
{
|
|
i->pluginAdded(plugin);
|
|
plugin.pluginAdded(*i);
|
|
}
|
|
}
|
|
|
|
void addPlugin(MousePlugin& plugin) override { m_mouse_plugins.push(&plugin); }
|
|
void removePlugin(GUIPlugin& plugin) override { m_gui_plugins.swapAndPopItem(&plugin); }
|
|
void removePlugin(MousePlugin& plugin) override { m_mouse_plugins.swapAndPopItem(&plugin); }
|
|
|
|
void runScript(const char* src, const char* script_name) override
|
|
{
|
|
lua_State* L = m_engine->getState();
|
|
bool errors = LuaWrapper::luaL_loadbuffer(L, src, stringLength(src), script_name) != 0;
|
|
errors = errors || lua_pcall(L, 0, 0, 0) != 0;
|
|
if (errors)
|
|
{
|
|
logError(script_name, ": ", lua_tostring(L, -1));
|
|
lua_pop(L, 1);
|
|
}
|
|
}
|
|
|
|
|
|
void savePrefabAs(const char* path) {
|
|
auto& selected_entities = m_editor->getSelectedEntities();
|
|
if (selected_entities.size() != 1) return;
|
|
|
|
EntityRef entity = selected_entities[0];
|
|
m_editor->getPrefabSystem().savePrefab(entity, Path(path));
|
|
}
|
|
|
|
|
|
void destroyEntity(EntityRef e) { m_editor->destroyEntities(&e, 1); }
|
|
|
|
|
|
void selectEntity(EntityRef e) { m_editor->selectEntities(Span(&e, 1), false); }
|
|
|
|
|
|
EntityRef createEntity() { return m_editor->addEntity(); }
|
|
|
|
void createComponent(EntityRef e, const char* type)
|
|
{
|
|
const ComponentType cmp_type = reflection::getComponentType(type);
|
|
m_editor->addComponent(Span(&e, 1), cmp_type);
|
|
}
|
|
|
|
i32 getSelectedEntitiesCount() const { return m_editor->getSelectedEntities().size(); }
|
|
EntityRef getSelectedEntity(u32 idx) const { return m_editor->getSelectedEntities()[idx]; }
|
|
|
|
void exitGameMode() { m_deferred_game_mode_exit = true; }
|
|
|
|
|
|
void exitWithCode(int exit_code)
|
|
{
|
|
m_finished = true;
|
|
m_exit_code = exit_code;
|
|
}
|
|
|
|
|
|
struct SetPropertyVisitor : reflection::IPropertyVisitor
|
|
{
|
|
void visit(const reflection::Property<int>& prop) override
|
|
{
|
|
if (!equalStrings(property_name, prop.name)) return;
|
|
if (!lua_isnumber(L, -1)) return;
|
|
|
|
if(reflection::getAttribute(prop, reflection::IAttribute::ENUM)) {
|
|
notSupported(prop);
|
|
}
|
|
|
|
int val = (int)lua_tointeger(L, -1);
|
|
editor->setProperty(cmp_type, "", 0, prop.name, Span(&entity, 1), val);
|
|
}
|
|
|
|
void visit(const reflection::Property<u32>& prop) override
|
|
{
|
|
if (!equalStrings(property_name, prop.name)) return;
|
|
if (!lua_isnumber(L, -1)) return;
|
|
|
|
const u32 val = (u32)lua_tointeger(L, -1);
|
|
editor->setProperty(cmp_type, "", 0, prop.name, Span(&entity, 1), val);
|
|
}
|
|
|
|
void visit(const reflection::Property<float>& prop) override
|
|
{
|
|
if (!equalStrings(property_name, prop.name)) return;
|
|
if (!lua_isnumber(L, -1)) return;
|
|
|
|
float val = (float)lua_tonumber(L, -1);
|
|
editor->setProperty(cmp_type, "", 0, prop.name, Span(&entity, 1), val);
|
|
}
|
|
|
|
void visit(const reflection::Property<Vec2>& prop) override
|
|
{
|
|
if (!equalStrings(property_name, prop.name)) return;
|
|
if (!LuaWrapper::isType<Vec2>(L, -1)) return;
|
|
|
|
const Vec2 val = LuaWrapper::toType<Vec2>(L, -1);
|
|
editor->setProperty(cmp_type, "", 0, prop.name, Span(&entity, 1), val);
|
|
}
|
|
|
|
void visit(const reflection::Property<Vec3>& prop) override
|
|
{
|
|
if (!equalStrings(property_name, prop.name)) return;
|
|
if (!LuaWrapper::isType<Vec3>(L, -1)) return;
|
|
|
|
const Vec3 val = LuaWrapper::toType<Vec3>(L, -1);
|
|
editor->setProperty(cmp_type, "", 0, prop.name, Span(&entity, 1), val);
|
|
}
|
|
|
|
void visit(const reflection::Property<IVec3>& prop) override
|
|
{
|
|
if (!equalStrings(property_name, prop.name)) return;
|
|
if (!LuaWrapper::isType<IVec3>(L, -1)) return;
|
|
|
|
const IVec3 val = LuaWrapper::toType<IVec3>(L, -1);
|
|
editor->setProperty(cmp_type, "", 0, prop.name, Span(&entity, 1), val);
|
|
}
|
|
|
|
void visit(const reflection::Property<Vec4>& prop) override
|
|
{
|
|
if (!equalStrings(property_name, prop.name)) return;
|
|
if (!LuaWrapper::isType<Vec4>(L, -1)) return;
|
|
|
|
const Vec4 val = LuaWrapper::toType<Vec4>(L, -1);
|
|
editor->setProperty(cmp_type, "", 0, prop.name, Span(&entity, 1), val);
|
|
}
|
|
|
|
void visit(const reflection::Property<const char*>& prop) override
|
|
{
|
|
if (!equalStrings(property_name, prop.name)) return;
|
|
if (!lua_isstring(L, -1)) return;
|
|
|
|
const char* str = lua_tostring(L, -1);
|
|
editor->setProperty(cmp_type, "", 0, prop.name, Span(&entity, 1), str);
|
|
}
|
|
|
|
|
|
void visit(const reflection::Property<Path>& prop) override
|
|
{
|
|
if (!equalStrings(property_name, prop.name)) return;
|
|
if (!lua_isstring(L, -1)) return;
|
|
|
|
const char* str = lua_tostring(L, -1);
|
|
editor->setProperty(cmp_type, "", 0, prop.name, Span(&entity, 1), Path(str));
|
|
}
|
|
|
|
|
|
void visit(const reflection::Property<bool>& prop) override
|
|
{
|
|
if (!equalStrings(property_name, prop.name)) return;
|
|
if (!lua_isboolean(L, -1)) return;
|
|
|
|
bool val = lua_toboolean(L, -1) != 0;
|
|
editor->setProperty(cmp_type, "", 0, prop.name, Span(&entity, 1), val);
|
|
}
|
|
|
|
void visit(const reflection::Property<EntityPtr>& prop) override { notSupported(prop); }
|
|
void visit(const reflection::ArrayProperty& prop) override { notSupported(prop); }
|
|
void visit(const reflection::BlobProperty& prop) override { notSupported(prop); }
|
|
|
|
template <typename T>
|
|
void notSupported(const T& prop)
|
|
{
|
|
if (!equalStrings(property_name, prop.name)) return;
|
|
logError("Property ", prop.name, " has unsupported type");
|
|
}
|
|
|
|
|
|
lua_State* L;
|
|
EntityRef entity;
|
|
ComponentType cmp_type;
|
|
const char* property_name;
|
|
WorldEditor* editor;
|
|
};
|
|
|
|
void guiAllActions() {
|
|
if (m_show_all_actions_request) ImGui::OpenPopup("Action palette");
|
|
|
|
if (ImGuiEx::BeginResizablePopup("Action palette", ImVec2(300, 200))) {
|
|
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) ImGui::CloseCurrentPopup();
|
|
|
|
if(m_show_all_actions_request) m_all_actions_selected = 0;
|
|
if (m_all_actions_filter.gui(ICON_FA_SEARCH " Search", -1, m_show_all_actions_request)) {
|
|
m_all_actions_selected = 0;
|
|
}
|
|
bool scroll = false;
|
|
const bool insert_enter = ImGui::IsItemFocused() && ImGui::IsKeyPressed(ImGuiKey_Enter);
|
|
if (ImGui::IsItemFocused()) {
|
|
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && m_all_actions_selected > 0) {
|
|
--m_all_actions_selected;
|
|
scroll = true;
|
|
}
|
|
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
|
|
++m_all_actions_selected;
|
|
scroll = true;
|
|
}
|
|
}
|
|
if (m_all_actions_filter.isActive()) {
|
|
if (ImGui::BeginChild("##list")) {
|
|
u32 idx = 0;
|
|
for (Action* act : m_actions) {
|
|
if (!m_all_actions_filter.pass(act->label_long)) continue;
|
|
|
|
char buf[20] = " (";
|
|
getShortcut(*act, Span(buf + 2, sizeof(buf) - 2));
|
|
if (buf[2]) {
|
|
catString(buf, ")");
|
|
}
|
|
else {
|
|
buf[0] = '\0';
|
|
}
|
|
bool selected = idx == m_all_actions_selected;
|
|
if (ImGui::Selectable(StaticString<128>(act->font_icon, act->label_long, buf), selected) || (selected && insert_enter)) {
|
|
ImGui::CloseCurrentPopup();
|
|
GUIPlugin* window = getFocusedWindow();
|
|
if (!window || !window->onAction(*act)) {
|
|
if (act->func.isValid()) {
|
|
act->func.invoke();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
++idx;
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
m_show_all_actions_request = false;
|
|
}
|
|
|
|
static int LUA_createEntityEx(lua_State* L) {
|
|
StudioAppImpl* studio = LuaWrapper::getClosureObject<StudioAppImpl>(L);
|
|
LuaWrapper::checkTableArg(L, 1);
|
|
|
|
WorldEditor& editor = *studio->m_editor;
|
|
editor.beginCommandGroup("createEntityEx");
|
|
EntityRef e = editor.addEntity();
|
|
editor.selectEntities(Span(&e, 1), false);
|
|
|
|
lua_pushvalue(L, 1);
|
|
lua_pushnil(L);
|
|
while (lua_next(L, -2) != 0)
|
|
{
|
|
const char* parameter_name = LuaWrapper::toType<const char*>(L, -2);
|
|
if (equalStrings(parameter_name, "name"))
|
|
{
|
|
const char* name = LuaWrapper::toType<const char*>(L, -1);
|
|
editor.setEntityName(e, name);
|
|
}
|
|
else if (equalStrings(parameter_name, "position"))
|
|
{
|
|
const DVec3 pos = LuaWrapper::toType<DVec3>(L, -1);
|
|
editor.setEntitiesPositions(&e, &pos, 1);
|
|
}
|
|
else if (equalStrings(parameter_name, "rotation"))
|
|
{
|
|
const Quat rot = LuaWrapper::toType<Quat>(L, -1);
|
|
editor.setEntitiesRotations(&e, &rot, 1);
|
|
}
|
|
else
|
|
{
|
|
ComponentType cmp_type = reflection::getComponentType(parameter_name);
|
|
editor.addComponent(Span(&e, 1), cmp_type);
|
|
|
|
IModule* module = editor.getWorld()->getModule(cmp_type);
|
|
if (module)
|
|
{
|
|
ComponentUID cmp(e, cmp_type, module);
|
|
const reflection::ComponentBase* cmp_des = reflection::getComponent(cmp_type);
|
|
if (cmp.isValid())
|
|
{
|
|
lua_pushvalue(L, -1);
|
|
lua_pushnil(L);
|
|
while (lua_next(L, -2) != 0)
|
|
{
|
|
const char* property_name = LuaWrapper::toType<const char*>(L, -2);
|
|
SetPropertyVisitor v;
|
|
v.property_name = property_name;
|
|
v.entity = (EntityRef)cmp.entity;
|
|
v.cmp_type = cmp.type;
|
|
v.L = L;
|
|
v.editor = &editor;
|
|
cmp_des->visit(v);
|
|
|
|
lua_pop(L, 1);
|
|
}
|
|
lua_pop(L, 1);
|
|
}
|
|
}
|
|
}
|
|
lua_pop(L, 1);
|
|
}
|
|
lua_pop(L, 1);
|
|
|
|
editor.endCommandGroup();
|
|
LuaWrapper::pushEntity(L, e, editor.getWorld());
|
|
return 1;
|
|
}
|
|
|
|
static int LUA_getSelectedEntity(lua_State* L) {
|
|
LuaWrapper::DebugGuard guard(L, 1);
|
|
i32 entity_idx = LuaWrapper::checkArg<i32>(L, 1);
|
|
|
|
StudioAppImpl* inst = LuaWrapper::getClosureObject<StudioAppImpl>(L);
|
|
EntityRef entity = inst->m_editor->getSelectedEntities()[entity_idx];
|
|
|
|
lua_getglobal(L, "Lumix");
|
|
lua_getfield(L, -1, "Entity");
|
|
lua_remove(L, -2);
|
|
lua_getfield(L, -1, "new");
|
|
lua_pushvalue(L, -2); // [Lumix.Entity, Entity.new, Lumix.Entity]
|
|
lua_remove(L, -3); // [Entity.new, Lumix.Entity]
|
|
World* world = inst->m_editor->getWorld();
|
|
LuaWrapper::push(L, world); // [Entity.new, Lumix.Entity, world]
|
|
LuaWrapper::push(L, entity.index); // [Entity.new, Lumix.Entity, world, entity_index]
|
|
const bool error = !LuaWrapper::pcall(L, 3, 1); // [entity]
|
|
return error ? 0 : 1;
|
|
}
|
|
|
|
static int LUA_getResources(lua_State* L)
|
|
{
|
|
auto* studio = LuaWrapper::checkArg<StudioAppImpl*>(L, 1);
|
|
auto* type = LuaWrapper::checkArg<const char*>(L, 2);
|
|
|
|
AssetCompiler& compiler = studio->getAssetCompiler();
|
|
if (!ResourceType(type).isValid()) return 0;
|
|
const auto& resources = compiler.lockResources();
|
|
|
|
lua_createtable(L, resources.size(), 0);
|
|
int i = 0;
|
|
for (const AssetCompiler::ResourceItem& res : resources)
|
|
{
|
|
LuaWrapper::push(L, res.path.c_str());
|
|
lua_rawseti(L, -2, i + 1);
|
|
++i;
|
|
}
|
|
|
|
compiler.unlockResources();
|
|
return 1;
|
|
}
|
|
|
|
|
|
void registerLuaAPI()
|
|
{
|
|
lua_State* L = m_engine->getState();
|
|
|
|
LuaWrapper::createSystemVariable(L, "Editor", "editor", this);
|
|
|
|
#define REGISTER_FUNCTION(F) \
|
|
do \
|
|
{ \
|
|
auto f = &LuaWrapper::wrapMethodClosure<&StudioAppImpl::F>; \
|
|
LuaWrapper::createSystemClosure(L, "Editor", this, #F, f); \
|
|
} while (false)
|
|
|
|
REGISTER_FUNCTION(savePrefabAs);
|
|
REGISTER_FUNCTION(selectEntity);
|
|
REGISTER_FUNCTION(createEntity);
|
|
REGISTER_FUNCTION(createComponent);
|
|
REGISTER_FUNCTION(destroyEntity);
|
|
REGISTER_FUNCTION(newWorld);
|
|
REGISTER_FUNCTION(exitWithCode);
|
|
REGISTER_FUNCTION(exitGameMode);
|
|
REGISTER_FUNCTION(getSelectedEntitiesCount);
|
|
|
|
#undef REGISTER_FUNCTION
|
|
|
|
LuaWrapper::createSystemClosure(L, "Editor", this, "getSelectedEntity", &LUA_getSelectedEntity);
|
|
LuaWrapper::createSystemFunction(L, "Editor", "getResources", &LUA_getResources);
|
|
LuaWrapper::createSystemClosure(L, "Editor", this, "createEntityEx", &LUA_createEntityEx);
|
|
}
|
|
|
|
void checkScriptCommandLine() {
|
|
char command_line[1024];
|
|
os::getCommandLine(Span(command_line));
|
|
CommandLineParser parser(command_line);
|
|
while (parser.next()) {
|
|
if (parser.currentEquals("-run_script")) {
|
|
if (!parser.next()) break;
|
|
|
|
char tmp[MAX_PATH];
|
|
parser.getCurrent(tmp, lengthOf(tmp));
|
|
OutputMemoryStream content(m_allocator);
|
|
|
|
if (m_engine->getFileSystem().getContentSync(Path(tmp), content)) {
|
|
content.write('\0');
|
|
runScript((const char*)content.data(), tmp);
|
|
}
|
|
else {
|
|
logError("Could not read ", tmp);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool includeFileInExport(const char* filename) {
|
|
if (filename[0] == '.') return false;
|
|
if (startsWith(filename, "bin/")) return false;
|
|
if (equalStrings("main.pak", filename)) return false;
|
|
if (equalStrings("error.log", filename)) return false;
|
|
return true;
|
|
}
|
|
|
|
static bool includeDirInExport(const char* filename) {
|
|
if (filename[0] == '.') return false;
|
|
if (startsWith(filename, "bin") == 0) return false;
|
|
return true;
|
|
}
|
|
|
|
struct ExportFileInfo {
|
|
FilePathHash hash;
|
|
u64 offset;
|
|
u64 size;
|
|
|
|
char path[MAX_PATH];
|
|
};
|
|
|
|
void scanCompiled(AssociativeArray<FilePathHash, ExportFileInfo>& infos) {
|
|
os::FileIterator* iter = m_engine->getFileSystem().createFileIterator(".lumix/resources");
|
|
const char* base_path = m_engine->getFileSystem().getBasePath();
|
|
os::FileInfo info;
|
|
exportFile("lumix.prj", infos);
|
|
while (os::getNextFile(iter, &info)) {
|
|
if (info.is_directory) continue;
|
|
|
|
StringView basename = Path::getBasename(info.filename);
|
|
ExportFileInfo rec;
|
|
u64 tmp_hash;
|
|
fromCString(basename, tmp_hash);
|
|
rec.hash = FilePathHash::fromU64(tmp_hash);
|
|
rec.offset = 0;
|
|
const Path path(base_path, ".lumix/resources/", info.filename);
|
|
rec.size = os::getFileSize(path);
|
|
copyString(rec.path, ".lumix/resources/");
|
|
catString(rec.path, info.filename);
|
|
infos.insert(rec.hash, rec);
|
|
}
|
|
exportDataScan("pipelines/", infos);
|
|
exportDataScan("universes/", infos);
|
|
|
|
os::destroyFileIterator(iter);
|
|
}
|
|
|
|
|
|
void exportFile(const char* file_path, AssociativeArray<FilePathHash, ExportFileInfo>& infos) {
|
|
const char* base_path = m_engine->getFileSystem().getBasePath();
|
|
const FilePathHash hash(file_path);
|
|
ExportFileInfo& out_info = infos.emplace(hash);
|
|
copyString(out_info.path, file_path);
|
|
out_info.hash = hash;
|
|
const Path path(base_path, file_path);
|
|
out_info.size = os::getFileSize(path);
|
|
out_info.offset = ~0UL;
|
|
}
|
|
|
|
void exportDataScan(const char* dir_path, AssociativeArray<FilePathHash, ExportFileInfo>& infos)
|
|
{
|
|
auto* iter = m_engine->getFileSystem().createFileIterator(dir_path);
|
|
const char* base_path = m_engine->getFileSystem().getBasePath();
|
|
os::FileInfo info;
|
|
while (os::getNextFile(iter, &info)) {
|
|
char normalized_path[MAX_PATH];
|
|
Path::normalize(info.filename, Span(normalized_path));
|
|
if (info.is_directory)
|
|
{
|
|
if (!includeDirInExport(normalized_path)) continue;
|
|
|
|
char dir[MAX_PATH] = {0};
|
|
if (dir_path[0] != '.') copyString(dir, dir_path);
|
|
catString(dir, info.filename);
|
|
catString(dir, "/");
|
|
exportDataScan(dir, infos);
|
|
continue;
|
|
}
|
|
|
|
if (!includeFileInExport(normalized_path)) continue;
|
|
|
|
StaticString<MAX_PATH> out_path;
|
|
if (dir_path[0] == '.')
|
|
{
|
|
copyString(out_path.data, normalized_path);
|
|
}
|
|
else
|
|
{
|
|
copyString(out_path.data, dir_path);
|
|
catString(out_path.data, normalized_path);
|
|
}
|
|
const FilePathHash hash(out_path.data);
|
|
if (infos.find(hash) >= 0) continue;
|
|
|
|
auto& out_info = infos.emplace(hash);
|
|
copyString(out_info.path, out_path);
|
|
out_info.hash = hash;
|
|
const Path path(base_path, out_path);
|
|
out_info.size = os::getFileSize(path);
|
|
out_info.offset = ~0UL;
|
|
}
|
|
os::destroyFileIterator(iter);
|
|
}
|
|
|
|
|
|
void exportDataScanResources(AssociativeArray<FilePathHash, ExportFileInfo>& infos)
|
|
{
|
|
ResourceManagerHub& rm = m_engine->getResourceManager();
|
|
exportFile("lumix.prj", infos);
|
|
for (auto iter = rm.getAll().begin(), end = rm.getAll().end(); iter != end; ++iter) {
|
|
const auto& resources = iter.value()->getResourceTable();
|
|
for (Resource* res : resources) {
|
|
const FilePathHash hash = res->getPath().getHash();
|
|
const Path baked_path(".lumix/resources/", hash, ".res");
|
|
|
|
if (infos.find(hash) < 0) {
|
|
auto& out_info = infos.emplace(hash);
|
|
copyString(Span(out_info.path), baked_path);
|
|
out_info.hash = hash;
|
|
out_info.size = os::getFileSize(baked_path);
|
|
out_info.offset = ~0UL;
|
|
}
|
|
}
|
|
}
|
|
exportDataScan("pipelines/", infos);
|
|
exportDataScan("universes/", infos);
|
|
}
|
|
|
|
|
|
void showExportGameDialog() { m_is_export_game_dialog_open = true; }
|
|
|
|
|
|
void guiExportData() {
|
|
if (!m_is_export_game_dialog_open) {
|
|
m_export_msg_timer = -1;
|
|
return;
|
|
}
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_FirstUseEver);
|
|
if (ImGui::Begin("Export game", &m_is_export_game_dialog_open)) {
|
|
ImGuiEx::Label("Destination dir");
|
|
if (ImGui::Button(m_export.dest_dir.empty() ? "..." : m_export.dest_dir)) {
|
|
if (os::getOpenDirectory(Span(m_export.dest_dir.data), m_engine->getFileSystem().getBasePath())) {
|
|
m_settings.setValue(Settings::LOCAL, "export_dir", m_export.dest_dir);
|
|
}
|
|
}
|
|
|
|
ImGuiEx::Label("Pack data");
|
|
if (ImGui::Checkbox("##pack", &m_export.pack)) {
|
|
m_settings.setValue(Settings::LOCAL, "export_pack", m_export.pack);
|
|
}
|
|
ImGuiEx::Label("Mode");
|
|
if (ImGui::Combo("##mode", (int*)&m_export.mode, "All files\0Loaded world\0")) {
|
|
m_settings.setValue(Settings::LOCAL, "export_pack", (i32)m_export.mode);
|
|
}
|
|
|
|
ImGuiEx::Label("Startup world");
|
|
if (ImGui::BeginCombo("##startunv", m_export.startup_world.c_str())) {
|
|
forEachWorld([&](const Path& path){
|
|
if (ImGui::Selectable(path.c_str())) m_export.startup_world = path;
|
|
});
|
|
ImGui::EndCombo();
|
|
}
|
|
if (m_export.startup_world.isEmpty()) {
|
|
forEachWorld([&](const Path& path){
|
|
if (m_export.startup_world.isEmpty()) {
|
|
m_export.startup_world = path;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (m_export_msg_timer > 0) {
|
|
m_export_msg_timer -= m_engine->getLastTimeDelta();
|
|
if (ImGui::Button("Export finished")) m_export_msg_timer = -1;
|
|
}
|
|
else {
|
|
if (ImGui::Button("Export")) {
|
|
if (exportData()) m_export_msg_timer = 3.f;
|
|
}
|
|
}
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
|
|
bool exportData() {
|
|
if (m_export.dest_dir.empty()) return false;
|
|
|
|
FileSystem& fs = m_engine->getFileSystem();
|
|
{
|
|
OutputMemoryStream prj_blob(m_allocator);
|
|
m_engine->serializeProject(prj_blob, m_export.startup_world);
|
|
|
|
const Path prj_file("lumix.prj");
|
|
if (!fs.saveContentSync(prj_file, prj_blob)) {
|
|
logError("Could not save ", prj_file);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
AssociativeArray<FilePathHash, ExportFileInfo> infos(m_allocator);
|
|
infos.reserve(10000);
|
|
|
|
switch (m_export.mode) {
|
|
case ExportConfig::Mode::ALL_FILES: scanCompiled(infos); break;
|
|
case ExportConfig::Mode::CURRENT_WORLD: exportDataScanResources(infos); break;
|
|
}
|
|
|
|
if (m_export.pack) {
|
|
StaticString<MAX_PATH> dest(m_export.dest_dir, "main.pak");
|
|
if (infos.size() == 0) {
|
|
logError("No files found while trying to create ", dest);
|
|
return false;
|
|
}
|
|
u64 total_size = 0;
|
|
for (ExportFileInfo& info : infos) {
|
|
info.offset = total_size;
|
|
total_size += info.size;
|
|
}
|
|
|
|
os::OutputFile file;
|
|
if (!file.open(dest)) {
|
|
logError("Could not create ", dest);
|
|
return false;
|
|
}
|
|
|
|
const u32 count = (u32)infos.size();
|
|
bool success = file.write(&count, sizeof(count));
|
|
|
|
for (auto& info : infos) {
|
|
success = file.write(&info.hash, sizeof(info.hash)) && success;
|
|
success = file.write(&info.offset, sizeof(info.offset)) && success;
|
|
success = file.write(&info.size, sizeof(info.size)) && success;
|
|
}
|
|
|
|
OutputMemoryStream src(m_allocator);
|
|
for (const ExportFileInfo& info : infos) {
|
|
src.clear();
|
|
if (!fs.getContentSync(Path(info.path), src)) {
|
|
logError("Could not read ", info.path);
|
|
file.close();
|
|
return false;
|
|
}
|
|
success = file.write(src.data(), src.size()) && success;
|
|
}
|
|
file.close();
|
|
|
|
if (!success) {
|
|
logError("Could not write ", dest);
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
const char* base_path = fs.getBasePath();
|
|
for (auto& info : infos) {
|
|
const Path src(base_path, info.path);
|
|
StaticString<MAX_PATH> dst(m_export.dest_dir, info.path);
|
|
StaticString<MAX_PATH> dst_dir(m_export.dest_dir, Path::getDir(info.path));
|
|
if (!os::makePath(dst_dir) && !os::dirExists(dst_dir)) {
|
|
logError("Failed to create ", dst_dir);
|
|
return false;
|
|
}
|
|
|
|
if (!os::copyFile(src, dst)) {
|
|
logError("Failed to copy ", src, " to ", dst);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
const char* bin_files[] = {"app.exe", "dbghelp.dll", "dbgcore.dll"};
|
|
StaticString<MAX_PATH> src_dir("bin/");
|
|
if (!os::fileExists("bin/app.exe")) {
|
|
char tmp[MAX_PATH];
|
|
os::getExecutablePath(Span(tmp));
|
|
copyString(Span(src_dir.data), Path::getDir(tmp));
|
|
}
|
|
|
|
for (auto& file : bin_files) {
|
|
StaticString<MAX_PATH> tmp(m_export.dest_dir, file);
|
|
StaticString<MAX_PATH> src(src_dir, file);
|
|
if (!os::copyFile(src, tmp)) {
|
|
logError("Failed to copy ", src, " to ", tmp);
|
|
}
|
|
}
|
|
|
|
for (GUIPlugin* plugin : m_gui_plugins) {
|
|
if (!plugin->exportData(m_export.dest_dir)) {
|
|
logError("Plugin ", plugin->getName(), " failed to pack data.");
|
|
}
|
|
}
|
|
logInfo("Exporting finished.");
|
|
return true;
|
|
}
|
|
|
|
Span<const os::Event> getEvents() const override { return m_events; }
|
|
|
|
void checkShortcuts() {
|
|
u8 pressed_modifiers = 0;
|
|
if (os::isKeyDown(os::Keycode::SHIFT)) pressed_modifiers |= Action::Modifiers::SHIFT;
|
|
if (os::isKeyDown(os::Keycode::CTRL)) pressed_modifiers |= Action::Modifiers::CTRL;
|
|
if (os::isKeyDown(os::Keycode::ALT)) pressed_modifiers |= Action::Modifiers::ALT;
|
|
GUIPlugin* window = getFocusedWindow();
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
for (Action*& a : m_actions) {
|
|
if (a->type == Action::LOCAL) continue;
|
|
if (a->type == Action::IMGUI_PRIORITY && io.WantCaptureKeyboard) continue;
|
|
if (a->shortcut == os::Keycode::INVALID && a->modifiers == 0) continue;
|
|
if (a->shortcut != os::Keycode::INVALID && !os::isKeyDown(a->shortcut)) continue;
|
|
if (a->modifiers != pressed_modifiers) continue;
|
|
|
|
if (window && window->onAction(*a))
|
|
return;
|
|
|
|
if (a->func.isValid()) {
|
|
a->func.invoke();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
IAllocator& getAllocator() override { return m_allocator; }
|
|
Engine& getEngine() override { return *m_engine; }
|
|
|
|
WorldEditor& getWorldEditor() override
|
|
{
|
|
ASSERT(m_editor.get());
|
|
return *m_editor;
|
|
}
|
|
|
|
int getImGuiKey(int keycode) const override{
|
|
return m_imgui_key_map[keycode];
|
|
}
|
|
|
|
ImFont* getDefaultFont() override { return m_font; }
|
|
ImFont* getBigIconFont() override { return m_big_icon_font; }
|
|
ImFont* getBoldFont() override { return m_bold_font; }
|
|
ImFont* getMonospaceFont() override { return m_monospace_font; }
|
|
|
|
struct WindowToDestroy {
|
|
os::WindowHandle window;
|
|
u32 counter;
|
|
};
|
|
|
|
DefaultAllocator m_main_allocator;
|
|
debug::Allocator m_debug_allocator;
|
|
TagAllocator m_allocator;
|
|
TagAllocator m_imgui_allocator;
|
|
|
|
UniquePtr<Engine> m_engine;
|
|
UniquePtr<WorldEditor> m_editor;
|
|
ImGuiKey m_imgui_key_map[255];
|
|
Array<os::WindowHandle> m_windows;
|
|
u32 m_frames_since_focused = 0;
|
|
Array<WindowToDestroy> m_deferred_destroy_windows;
|
|
os::WindowHandle m_main_window;
|
|
os::WindowState m_fullscreen_restore_state;
|
|
jobs::Signal m_init_imgui_signal;
|
|
|
|
Array<Action*> m_owned_actions;
|
|
Array<Action*> m_tools_actions;
|
|
Array<Action*> m_actions;
|
|
Array<Action*> m_window_actions;
|
|
|
|
CommonActions m_common_actions;
|
|
Action m_show_all_actions_action;
|
|
|
|
Array<GUIPlugin*> m_gui_plugins;
|
|
Array<MousePlugin*> m_mouse_plugins;
|
|
Array<IPlugin*> m_plugins;
|
|
Array<IAddComponentPlugin*> m_add_cmp_plugins;
|
|
|
|
AddCmpTreeNode m_add_cmp_root;
|
|
HashMap<ComponentType, String> m_component_labels;
|
|
HashMap<ComponentType, StaticString<5>> m_component_icons;
|
|
Gizmo::Config m_gizmo_config;
|
|
|
|
bool m_show_save_world_ui = false;
|
|
bool m_cursor_clipped = false;
|
|
bool m_confirm_exit = false;
|
|
bool m_confirm_load = false;
|
|
bool m_confirm_new = false;
|
|
bool m_confirm_destroy_partition = false;
|
|
bool m_is_caption_hovered = false;
|
|
|
|
World::PartitionHandle m_partition_to_destroy;
|
|
Path m_world_to_load;
|
|
|
|
ImTextureID m_logo = nullptr;
|
|
UniquePtr<AssetBrowser> m_asset_browser;
|
|
UniquePtr<AssetCompiler> m_asset_compiler;
|
|
Local<PropertyGrid> m_property_grid;
|
|
UniquePtr<GUIPlugin> m_profiler_ui;
|
|
Local<LogUI> m_log_ui;
|
|
Settings m_settings;
|
|
|
|
FileSelector m_file_selector;
|
|
DirSelector m_dir_selector;
|
|
|
|
float m_fov = degreesToRadians(60);
|
|
RenderInterface* m_render_interface = nullptr;
|
|
Array<os::Event> m_events;
|
|
TextFilter m_open_filter;
|
|
TextFilter m_component_filter;
|
|
|
|
float m_fps = 0;
|
|
os::Timer m_fps_timer;
|
|
os::Timer m_inactive_fps_timer;
|
|
u32 m_fps_frame = 0;
|
|
|
|
struct ExportConfig {
|
|
enum class Mode : i32 {
|
|
ALL_FILES,
|
|
CURRENT_WORLD
|
|
};
|
|
|
|
Mode mode = Mode::ALL_FILES;
|
|
|
|
bool pack = false;
|
|
Path startup_world;
|
|
StaticString<MAX_PATH> dest_dir;
|
|
};
|
|
|
|
ExportConfig m_export;
|
|
float m_export_msg_timer = -1;
|
|
bool m_entity_selection_changed = false;
|
|
bool m_finished;
|
|
bool m_deferred_game_mode_exit;
|
|
int m_exit_code;
|
|
|
|
bool m_is_welcome_screen_open;
|
|
bool m_is_export_game_dialog_open;
|
|
bool m_is_entity_list_open;
|
|
|
|
EntityPtr m_renaming_entity = INVALID_ENTITY;
|
|
EntityFolders::FolderHandle m_renaming_folder = EntityFolders::INVALID_FOLDER;
|
|
bool m_set_rename_focus = false;
|
|
char m_rename_buf[World::ENTITY_NAME_MAX_LENGTH];
|
|
bool m_is_f2_pressed = false;
|
|
|
|
ImFont* m_font;
|
|
ImFont* m_big_icon_font;
|
|
ImFont* m_bold_font;
|
|
ImFont* m_monospace_font;
|
|
ImGuiID m_dockspace_id = 0;
|
|
|
|
struct WatchedPlugin {
|
|
UniquePtr<FileSystemWatcher> watcher;
|
|
StaticString<MAX_PATH> dir;
|
|
StaticString<MAX_PATH> basename;
|
|
Lumix::ISystem* system = nullptr;
|
|
int iteration = 0;
|
|
bool reload_request = false;
|
|
} m_watched_plugin;
|
|
|
|
bool m_show_all_actions_request = false;
|
|
i32 m_all_actions_selected = 0;
|
|
TextFilter m_all_actions_filter;
|
|
};
|
|
|
|
static Local<StudioAppImpl> g_studio;
|
|
|
|
StudioApp* StudioApp::create()
|
|
{
|
|
g_studio.create();
|
|
return g_studio.get();
|
|
}
|
|
|
|
|
|
void StudioApp::destroy(StudioApp& app)
|
|
{
|
|
ASSERT(&app == g_studio.get());
|
|
g_studio.destroy();
|
|
}
|
|
|
|
|
|
} // namespace Lumix
|