778 lines
21 KiB
C++
778 lines
21 KiB
C++
#include <imgui/imgui.h>
|
|
|
|
#include "utils.h"
|
|
#include "editor/render_interface.h"
|
|
#include "editor/settings.h"
|
|
#include "editor/studio_app.h"
|
|
#include "editor/world_editor.h"
|
|
#include "engine/engine.h"
|
|
#include "engine/file_system.h"
|
|
#include "engine/log.h"
|
|
#include "engine/math.h"
|
|
#include "engine/os.h"
|
|
#include "engine/path.h"
|
|
#include "engine/world.h"
|
|
|
|
|
|
namespace Lumix
|
|
{
|
|
|
|
|
|
ResourceLocator::ResourceLocator(const Span<const char>& path)
|
|
{
|
|
full = path;
|
|
const char* c = path.m_begin;
|
|
subresource.m_begin = c;
|
|
while(c != path.m_end && *c != ':') {
|
|
++c;
|
|
}
|
|
if(c != path.m_end) {
|
|
subresource.m_end = c;
|
|
dir.m_begin = c + 1;
|
|
}
|
|
else {
|
|
subresource.m_end = subresource.m_begin;
|
|
dir.m_begin = path.m_begin;
|
|
}
|
|
|
|
ext.m_end = path.m_end;
|
|
ext.m_begin = reverseFind(dir.m_begin, ext.m_end, '.');
|
|
if (ext.m_begin) {
|
|
basename.m_end = ext.m_begin;
|
|
++ext.m_begin;
|
|
}
|
|
else {
|
|
ext.m_begin = ext.m_end;
|
|
basename.m_end = path.m_end;
|
|
}
|
|
basename.m_begin = reverseFind(dir.m_begin, basename.m_end, '/');
|
|
if (!basename.m_begin) basename.m_begin = reverseFind(dir.m_begin, basename.m_end, '\\');
|
|
if (basename.m_begin) {
|
|
dir.m_end = basename.m_begin;
|
|
++basename.m_begin;
|
|
}
|
|
else {
|
|
basename.m_begin = dir.m_begin;
|
|
dir.m_end = dir.m_begin;
|
|
}
|
|
resource.m_begin = dir.m_begin;
|
|
resource.m_end = ext.m_end;
|
|
}
|
|
|
|
Action::Action() {
|
|
shortcut = os::Keycode::INVALID;
|
|
}
|
|
|
|
void Action::init(const char* label_short, const char* label_long, const char* name, const char* font_icon, bool is_global) {
|
|
this->label_long = label_long;
|
|
this->label_short = label_short;
|
|
this->font_icon = font_icon;
|
|
this->name = name;
|
|
this->is_global = is_global;
|
|
plugin = nullptr;
|
|
shortcut = os::Keycode::INVALID;
|
|
is_selected.bind<falseConst>();
|
|
}
|
|
|
|
|
|
void Action::init(const char* label_short,
|
|
const char* label_long,
|
|
const char* name,
|
|
const char* font_icon,
|
|
os::Keycode shortcut,
|
|
Modifiers modifiers,
|
|
bool is_global)
|
|
{
|
|
this->label_long = label_long;
|
|
this->label_short = label_short;
|
|
this->name = name;
|
|
this->font_icon = font_icon;
|
|
this->is_global = is_global;
|
|
this->shortcut = shortcut;
|
|
this->modifiers = modifiers;
|
|
plugin = nullptr;
|
|
is_selected.bind<falseConst>();
|
|
}
|
|
|
|
bool Action::shortcutText(Span<char> out) const {
|
|
if (shortcut == os::Keycode::INVALID && modifiers == 0) {
|
|
copyString(out, "");
|
|
return false;
|
|
}
|
|
char tmp[32];
|
|
os::getKeyName(shortcut, Span(tmp));
|
|
|
|
copyString(out, "");
|
|
if (modifiers & (u8)Action::Modifiers::CTRL) catString(out, "Ctrl ");
|
|
if (modifiers & (u8)Action::Modifiers::SHIFT) catString(out, "Shift ");
|
|
if (modifiers & (u8)Action::Modifiers::ALT) catString(out, "Alt ");
|
|
catString(out, shortcut == os::Keycode::INVALID ? "" : tmp);
|
|
const i32 len = stringLength(out.m_begin);
|
|
if (len > 0 && out[len - 1] == ' ') {
|
|
out[len - 1] = '\0';
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Action::toolbarButton(ImFont* font)
|
|
{
|
|
const ImVec4 col_active = ImGui::GetStyle().Colors[ImGuiCol_ButtonActive];
|
|
const ImVec4 bg_color = is_selected.invoke() ? col_active : ImGui::GetStyle().Colors[ImGuiCol_Text];
|
|
|
|
if (!font_icon[0]) return false;
|
|
|
|
ImGui::SameLine();
|
|
if(ImGuiEx::ToolbarButton(font, font_icon, bg_color, label_long)) {
|
|
func.invoke();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
bool Action::isActive()
|
|
{
|
|
if (ImGui::IsAnyItemFocused()) return false;
|
|
if (shortcut == os::Keycode::INVALID && modifiers == 0) return false;
|
|
|
|
if (shortcut != os::Keycode::INVALID && !os::isKeyDown(shortcut)) return false;
|
|
|
|
Modifiers pressed_modifiers = Modifiers::NONE;
|
|
if (os::isKeyDown(os::Keycode::MENU)) pressed_modifiers |= Modifiers::ALT;
|
|
if (os::isKeyDown(os::Keycode::SHIFT)) pressed_modifiers |= Modifiers::SHIFT;
|
|
if (os::isKeyDown(os::Keycode::CTRL)) pressed_modifiers |= Modifiers::CTRL;
|
|
if (modifiers != pressed_modifiers && modifiers != Modifiers::NONE) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
void getShortcut(const Action& action, Span<char> buf) {
|
|
buf[0] = 0;
|
|
|
|
if (action.modifiers & (u8)Action::Modifiers::CTRL) catString(buf, "CTRL ");
|
|
if (action.modifiers & (u8)Action::Modifiers::SHIFT) catString(buf, "SHIFT ");
|
|
if (action.modifiers & (u8)Action::Modifiers::ALT) catString(buf, "ALT ");
|
|
|
|
if (action.shortcut != os::Keycode::INVALID) {
|
|
char tmp[64];
|
|
os::getKeyName(action.shortcut, Span(tmp));
|
|
if (tmp[0] == 0) return;
|
|
catString(buf, tmp);
|
|
}
|
|
}
|
|
|
|
void menuItem(Action& a, bool enabled)
|
|
{
|
|
char buf[20];
|
|
getShortcut(a, Span(buf));
|
|
if (ImGui::MenuItem(a.label_short, buf, a.is_selected.invoke(), enabled))
|
|
{
|
|
a.func.invoke();
|
|
}
|
|
}
|
|
|
|
void getEntityListDisplayName(StudioApp& app, World& world, Span<char> buf, EntityPtr entity)
|
|
{
|
|
if (!entity.isValid())
|
|
{
|
|
buf[0] = '\0';
|
|
return;
|
|
}
|
|
|
|
EntityRef e = (EntityRef)entity;
|
|
const char* name = world.getEntityName(e);
|
|
static const auto MODEL_INSTANCE_TYPE = reflection::getComponentType("model_instance");
|
|
if (world.hasComponent(e, MODEL_INSTANCE_TYPE))
|
|
{
|
|
RenderInterface* render_interface = app.getRenderInterface();
|
|
const Path path = render_interface->getModelInstancePath(world, e);
|
|
if (!path.isEmpty())
|
|
{
|
|
const char* c = path.c_str();
|
|
while (*c && *c != ':') ++c;
|
|
if (*c == ':') {
|
|
copyNString(buf, path.c_str(), int(c - path.c_str() + 1));
|
|
return;
|
|
}
|
|
|
|
copyString(buf, path.c_str());
|
|
Span<const char> basename = Path::getBasename(path.c_str());
|
|
if (name && name[0] != '\0')
|
|
copyString(buf, name);
|
|
else
|
|
toCString(entity.index, buf);
|
|
|
|
catString(buf, " - ");
|
|
catString(buf, basename);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (name && name[0] != '\0')
|
|
{
|
|
copyString(buf, name);
|
|
}
|
|
else
|
|
{
|
|
toCString(entity.index, buf);
|
|
}
|
|
}
|
|
|
|
static int inputTextCallback(ImGuiInputTextCallbackData* data) {
|
|
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
|
|
String* str = (String*)data->UserData;
|
|
ASSERT(data->Buf == str->c_str());
|
|
str->resize(data->BufTextLen);
|
|
data->Buf = (char*)str->c_str();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool inputString(const char* label, String* value) {
|
|
ImGuiInputTextFlags flags = ImGuiInputTextFlags_CallbackResize;
|
|
return ImGui::InputText(label, (char*)value->c_str(), value->length() + 1, flags, inputTextCallback, value);
|
|
}
|
|
|
|
SimpleUndoRedo::SimpleUndoRedo(IAllocator& allocator)
|
|
: m_stack(allocator)
|
|
, m_allocator(allocator)
|
|
{}
|
|
|
|
bool SimpleUndoRedo::canUndo() const { return m_stack_idx > 0; }
|
|
bool SimpleUndoRedo::canRedo() const { return m_stack_idx < m_stack.size() - 1; }
|
|
|
|
void SimpleUndoRedo::undo() {
|
|
if (m_stack_idx <= 0) return;
|
|
|
|
InputMemoryStream blob(m_stack[m_stack_idx - 1].blob);
|
|
deserialize(blob);
|
|
--m_stack_idx;
|
|
}
|
|
|
|
void SimpleUndoRedo::redo() {
|
|
if (m_stack_idx + 1 >= m_stack.size()) return;
|
|
|
|
InputMemoryStream blob(m_stack[m_stack_idx + 1].blob);
|
|
deserialize(blob);
|
|
++m_stack_idx;
|
|
}
|
|
|
|
void SimpleUndoRedo::pushUndo(u32 tag) {
|
|
while (m_stack.size() > m_stack_idx + 1) m_stack.pop();
|
|
|
|
Undo u(m_allocator);
|
|
u.tag = tag;
|
|
serialize(u.blob);
|
|
if (tag == NO_MERGE_UNDO || m_stack.back().tag != tag) {
|
|
m_stack.push(static_cast<Undo&&>(u));
|
|
++m_stack_idx;
|
|
}
|
|
else {
|
|
m_stack.back() = static_cast<Undo&&>(u);
|
|
}
|
|
}
|
|
|
|
void SimpleUndoRedo::clearUndoStack() {
|
|
m_stack.clear();
|
|
m_stack_idx = -1;
|
|
}
|
|
|
|
void FileSelector::fillSubitems() {
|
|
m_subdirs.clear();
|
|
m_subfiles.clear();
|
|
FileSystem& fs = m_app.getEngine().getFileSystem();
|
|
const char* base_path = fs.getBasePath();
|
|
|
|
const Path path(base_path, "/", m_current_dir.c_str());
|
|
os::FileIterator* iter = os::createFileIterator(path, m_app.getAllocator());
|
|
os::FileInfo info;
|
|
const char* ext = m_accepted_extension.c_str();
|
|
while (os::getNextFile(iter, &info)) {
|
|
if (equalStrings(info.filename, ".")) continue;
|
|
if (equalStrings(info.filename, "..")) continue;
|
|
if (equalStrings(info.filename, ".lumix") && m_current_dir.length() == 0) continue;
|
|
|
|
if (info.is_directory) {
|
|
m_subdirs.emplace(info.filename, m_app.getAllocator());
|
|
}
|
|
else {
|
|
if (!ext[0] || Path::hasExtension(info.filename, ext)) {
|
|
m_subfiles.emplace(info.filename, m_app.getAllocator());
|
|
}
|
|
}
|
|
}
|
|
os::destroyFileIterator(iter);
|
|
}
|
|
|
|
|
|
bool FileSelector::breadcrumb(Span<const char> path) {
|
|
if (path.length() == 0) {
|
|
if (ImGui::Button(".")) {
|
|
m_current_dir = "";
|
|
fillSubitems();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
if (path.back() == '/') path = path.fromRight(1);
|
|
|
|
Span<const char> dir = Path::getDir(path);
|
|
Span<const char> basename = Path::getBasename(path);
|
|
if (breadcrumb(dir)) return true;
|
|
ImGui::SameLine();
|
|
ImGui::TextUnformatted("/");
|
|
ImGui::SameLine();
|
|
|
|
char tmp[LUMIX_MAX_PATH];
|
|
copyString(Span(tmp), basename);
|
|
if (ImGui::Button(tmp)) {
|
|
m_current_dir = String(path, m_app.getAllocator());
|
|
fillSubitems();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
FileSelector::FileSelector(const char* ext, StudioApp& app)
|
|
: m_app(app)
|
|
, m_filename(app.getAllocator())
|
|
, m_full_path(app.getAllocator())
|
|
, m_current_dir(app.getAllocator())
|
|
, m_subdirs(app.getAllocator())
|
|
, m_subfiles(app.getAllocator())
|
|
, m_accepted_extension(ext, app.getAllocator())
|
|
{
|
|
fillSubitems();
|
|
}
|
|
|
|
DirSelector::DirSelector(StudioApp& app)
|
|
: m_app(app)
|
|
, m_current_dir(app.getAllocator())
|
|
, m_subdirs(app.getAllocator())
|
|
{}
|
|
|
|
|
|
void DirSelector::fillSubitems() {
|
|
m_subdirs.clear();
|
|
FileSystem& fs = m_app.getEngine().getFileSystem();
|
|
const char* base_path = fs.getBasePath();
|
|
|
|
const Path path(base_path, "/", m_current_dir.c_str());
|
|
os::FileIterator* iter = os::createFileIterator(path, m_app.getAllocator());
|
|
os::FileInfo info;
|
|
while (os::getNextFile(iter, &info)) {
|
|
if (equalStrings(info.filename, ".")) continue;
|
|
if (equalStrings(info.filename, "..")) continue;
|
|
if (equalStrings(info.filename, ".lumix") && m_current_dir.length() == 0) continue;
|
|
|
|
if (info.is_directory) {
|
|
m_subdirs.emplace(info.filename, m_app.getAllocator());
|
|
}
|
|
}
|
|
os::destroyFileIterator(iter);
|
|
}
|
|
|
|
bool DirSelector::breadcrumb(Span<const char> path) {
|
|
if (path.length() == 0) {
|
|
if (ImGui::Button(".")) {
|
|
m_current_dir = "";
|
|
fillSubitems();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
if (path.back() == '/') path = path.fromRight(1);
|
|
|
|
Span<const char> dir = Path::getDir(path);
|
|
Span<const char> basename = Path::getBasename(path);
|
|
if (breadcrumb(dir)) return true;
|
|
ImGui::SameLine();
|
|
ImGui::TextUnformatted("/");
|
|
ImGui::SameLine();
|
|
|
|
char tmp[LUMIX_MAX_PATH];
|
|
copyString(Span(tmp), basename);
|
|
if (ImGui::Button(tmp)) {
|
|
m_current_dir = String(path, m_app.getAllocator());
|
|
fillSubitems();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool DirSelector::gui(const char* label, bool* open) {
|
|
if (*open && !ImGui::IsPopupOpen(label)) {
|
|
ImGui::OpenPopup(label);
|
|
fillSubitems();
|
|
}
|
|
|
|
if (ImGui::BeginPopupModal(label, nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
bool recently_open_create_folder = false;
|
|
if (ImGui::Button(ICON_FA_PLUS " Create folder")) {
|
|
m_creating_folder = true;
|
|
m_new_folder_name[0] = '\0';
|
|
recently_open_create_folder = true;
|
|
}
|
|
breadcrumb(Span(m_current_dir.c_str(), m_current_dir.length()));
|
|
if (ImGui::BeginChild("list", ImVec2(300, 300), true, ImGuiWindowFlags_NoScrollbar)) {
|
|
if (m_current_dir.length() > 0) {
|
|
if (ImGui::Selectable(ICON_FA_LEVEL_UP_ALT "..", false, ImGuiSelectableFlags_DontClosePopups)) {
|
|
Span<const char> dir = Path::getDir(m_current_dir.c_str());
|
|
if (dir.length() > 0) dir = dir.fromRight(1);
|
|
m_current_dir = String(dir, m_app.getAllocator());
|
|
fillSubitems();
|
|
}
|
|
}
|
|
|
|
if (m_creating_folder) {
|
|
ImGui::SetNextItemWidth(-1);
|
|
if (recently_open_create_folder) ImGui::SetKeyboardFocusHere();
|
|
ImGui::InputTextWithHint("##nf", "New folder name", m_new_folder_name, sizeof(m_new_folder_name), ImGuiInputTextFlags_AutoSelectAll);
|
|
if (ImGui::IsItemDeactivated()) {
|
|
m_creating_folder = false;
|
|
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
|
if (m_new_folder_name[0]) {
|
|
FileSystem& fs = m_app.getEngine().getFileSystem();
|
|
const Path fullpath(fs.getBasePath(), m_current_dir.c_str(), "/", m_new_folder_name);
|
|
if (!os::makePath(fullpath)) {
|
|
logError("Failed to create ", fullpath);
|
|
}
|
|
else {
|
|
m_current_dir.cat("/").cat(m_new_folder_name);
|
|
m_new_folder_name[0] = '\0';
|
|
}
|
|
fillSubitems();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const String& subdir : m_subdirs) {
|
|
ImGui::TextUnformatted(ICON_FA_FOLDER); ImGui::SameLine();
|
|
if (ImGui::Selectable(subdir.c_str(), false, ImGuiSelectableFlags_DontClosePopups)) {
|
|
m_current_dir.cat("/");
|
|
m_current_dir.cat(subdir.c_str());
|
|
fillSubitems();
|
|
}
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
|
|
bool res = ImGui::Button(ICON_FA_CHECK " Select");
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(ICON_FA_TIMES " Cancel")) ImGui::CloseCurrentPopup();
|
|
|
|
if (res) ImGui::CloseCurrentPopup();
|
|
ImGui::EndPopup();
|
|
if (!ImGui::IsPopupOpen(label)) *open = false;
|
|
return res;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
FileSelector::FileSelector(StudioApp& app)
|
|
: m_app(app)
|
|
, m_filename(app.getAllocator())
|
|
, m_full_path(app.getAllocator())
|
|
, m_current_dir(app.getAllocator())
|
|
, m_subdirs(app.getAllocator())
|
|
, m_subfiles(app.getAllocator())
|
|
, m_accepted_extension(app.getAllocator())
|
|
{}
|
|
|
|
const char* FileSelector::getPath() {
|
|
Span<const char> span(m_full_path.c_str(), m_full_path.length());
|
|
if (Path::getExtension(span).length() == 0) m_full_path.cat(".").cat(m_accepted_extension.c_str());
|
|
return m_full_path.c_str();
|
|
}
|
|
|
|
bool FileSelector::gui(bool show_breadcrumbs) {
|
|
bool res = false;
|
|
ImGui::TextUnformatted("Filename"); ImGui::SameLine(); ImGui::SetNextItemWidth(-1);
|
|
bool changed = inputString("##fn", &m_filename);
|
|
|
|
if (show_breadcrumbs) {
|
|
changed = breadcrumb(Span(m_current_dir.c_str(), m_current_dir.length())) || changed;
|
|
}
|
|
if (ImGui::BeginChild("list", ImVec2(300, 300), true, ImGuiWindowFlags_NoScrollbar)) {
|
|
if (m_current_dir.length() > 0) {
|
|
if (ImGui::Selectable(ICON_FA_LEVEL_UP_ALT "..", false, ImGuiSelectableFlags_DontClosePopups)) {
|
|
Span<const char> dir = Path::getDir(m_current_dir.c_str());
|
|
if (dir.length() > 0) dir = dir.fromRight(1);
|
|
m_current_dir = String(dir, m_app.getAllocator());
|
|
fillSubitems();
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
for (const String& subdir : m_subdirs) {
|
|
ImGui::TextUnformatted(ICON_FA_FOLDER); ImGui::SameLine();
|
|
if (ImGui::Selectable(subdir.c_str(), false, ImGuiSelectableFlags_DontClosePopups)) {
|
|
m_current_dir.cat("/");
|
|
m_current_dir.cat(subdir.c_str());
|
|
fillSubitems();
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
for (const String& subfile : m_subfiles) {
|
|
if (ImGui::Selectable(subfile.c_str(), false, ImGuiSelectableFlags_DontClosePopups | ImGuiSelectableFlags_AllowDoubleClick)) {
|
|
m_filename = subfile;
|
|
changed = true;
|
|
if (ImGui::IsMouseDoubleClicked(0)) {
|
|
res = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
if (changed) {
|
|
m_full_path = m_current_dir;
|
|
m_full_path.cat("/").cat(m_filename.c_str());
|
|
}
|
|
return res;
|
|
}
|
|
|
|
bool FileSelector::gui(const char* label, bool* open, const char* extension, bool save) {
|
|
if (*open && !ImGui::IsPopupOpen(label)) {
|
|
ImGui::OpenPopup(label);
|
|
m_save = save;
|
|
m_accepted_extension = extension;
|
|
m_filename = "";
|
|
m_full_path = "";
|
|
fillSubitems();
|
|
}
|
|
|
|
bool res = false;
|
|
if (ImGui::BeginPopupModal(label, nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
res = gui(true);
|
|
|
|
if (m_save) {
|
|
if (ImGui::Button(ICON_FA_SAVE " Save")) {
|
|
if (!Path::hasExtension(m_full_path.c_str(), m_accepted_extension.c_str())) {
|
|
m_full_path.cat(".").cat(m_accepted_extension.c_str());
|
|
}
|
|
if (m_app.getEngine().getFileSystem().fileExists(m_full_path.c_str())) {
|
|
ImGui::OpenPopup("warn_overwrite");
|
|
}
|
|
else {
|
|
res = true;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if (ImGui::Button(ICON_FA_FOLDER_OPEN " Open")) {
|
|
if (m_app.getEngine().getFileSystem().fileExists(m_full_path.c_str())) {
|
|
res = true;
|
|
}
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(ICON_FA_TIMES " Cancel")) ImGui::CloseCurrentPopup();
|
|
|
|
if (ImGui::BeginPopup("warn_overwrite")) {
|
|
ImGui::TextUnformatted("File already exists, are you sure you want to overwrite it?");
|
|
if (ImGui::Selectable("Yes")) res = true;
|
|
ImGui::Selectable("No");
|
|
ImGui::EndPopup();
|
|
}
|
|
if (res) ImGui::CloseCurrentPopup();
|
|
ImGui::EndPopup();
|
|
if (!ImGui::IsPopupOpen(label)) *open = false;
|
|
return res;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
enum { OUTPUT_FLAG = 1 << 31 };
|
|
|
|
NodeEditor::NodeEditor(IAllocator& allocator)
|
|
: SimpleUndoRedo(allocator)
|
|
{}
|
|
|
|
void NodeEditor::splitLink(const NodeEditorNode* node, Array<NodeEditorLink>& links, u32 link_idx) {
|
|
if (node->hasInputPins() && node->hasOutputPins()) {
|
|
NodeEditorLink& new_link = links.emplace();
|
|
NodeEditorLink& link = links[link_idx];
|
|
new_link.color = link.color;
|
|
new_link.to = link.to;
|
|
new_link.from = node->m_id;
|
|
link.to = node->m_id;
|
|
pushUndo(SimpleUndoRedo::NO_MERGE_UNDO);
|
|
}
|
|
}
|
|
|
|
void NodeEditor::nodeEditorGUI(Span<NodeEditorNode*> nodes, Array<NodeEditorLink>& links) {
|
|
m_canvas.begin();
|
|
|
|
ImGuiEx::BeginNodeEditor("node_editor", &m_offset);
|
|
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
|
|
|
ImGuiID moved = 0;
|
|
ImGuiID unlink_moved = 0;
|
|
u32 moved_count = 0;
|
|
u32 unlink_moved_count = 0;
|
|
for (NodeEditorNode* node : nodes) {
|
|
const ImVec2 old_pos = node->m_pos;
|
|
if (node->nodeGUI()) {
|
|
pushUndo(node->m_id);
|
|
}
|
|
if (ImGui::IsMouseDragging(0) && ImGui::IsItemHovered()) m_dragged_node = node->m_id;
|
|
if (old_pos.x != node->m_pos.x || old_pos.y != node->m_pos.y) {
|
|
moved = node->m_id;
|
|
++moved_count;
|
|
if (ImGui::GetIO().KeyAlt) {
|
|
u32 old_count = links.size();
|
|
|
|
for (i32 i = links.size() - 1; i >= 0; --i) {
|
|
const NodeEditorLink& link = links[i];
|
|
if (link.getToNode() == node->m_id) {
|
|
for (NodeEditorLink& rlink : links) {
|
|
if (rlink.getFromNode() == node->m_id && rlink.getFromPin() == link.getToPin()) {
|
|
rlink.from = link.from;
|
|
links.erase(i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
unlink_moved_count += old_count != links.size() ? 1 : 0;
|
|
unlink_moved = node->m_id;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (moved_count > 0) {
|
|
if (unlink_moved_count > 1) pushUndo(NO_MERGE_UNDO);
|
|
else if (unlink_moved_count == 1) pushUndo(unlink_moved);
|
|
else if (moved_count > 1) pushUndo(NO_MERGE_UNDO - 1);
|
|
else pushUndo(moved);
|
|
}
|
|
|
|
i32 hovered_link = -1;
|
|
for (i32 i = 0, c = links.size(); i < c; ++i) {
|
|
NodeEditorLink& link = links[i];
|
|
ImGuiEx::NodeLinkEx(link.from | OUTPUT_FLAG, link.to, link.color, ImGui::GetColorU32(ImGuiCol_TabActive));
|
|
if (ImGuiEx::IsLinkHovered()) {
|
|
if (ImGui::IsMouseClicked(0) && ImGui::GetIO().KeyCtrl) {
|
|
if (ImGuiEx::IsLinkStartHovered()) {
|
|
ImGuiEx::StartNewLink(link.to, true);
|
|
}
|
|
else {
|
|
ImGuiEx::StartNewLink(link.from | OUTPUT_FLAG, false);
|
|
}
|
|
links.erase(i);
|
|
--c;
|
|
}
|
|
if (ImGui::IsMouseDoubleClicked(0)) {
|
|
onLinkDoubleClicked(link, ImGui::GetMousePos() - origin - m_offset);
|
|
}
|
|
else {
|
|
hovered_link = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hovered_link >= 0 && ImGui::IsMouseReleased(0) && ImGui::GetIO().KeyAlt) {
|
|
i32 node_idx = nodes.find([this](const NodeEditorNode* node){ return node->m_id == m_dragged_node; });
|
|
if (node_idx >= 0) {
|
|
splitLink(nodes[node_idx], links, hovered_link);
|
|
}
|
|
}
|
|
|
|
if (ImGui::IsMouseReleased(0)) m_dragged_node = 0xffFFffFF;
|
|
|
|
{
|
|
ImGuiID start_attr, end_attr;
|
|
if (ImGuiEx::GetHalfLink(&start_attr)) {
|
|
m_half_link_start = start_attr;
|
|
}
|
|
|
|
if (ImGuiEx::GetNewLink(&start_attr, &end_attr)) {
|
|
ASSERT(start_attr & OUTPUT_FLAG);
|
|
links.eraseItems([&](const NodeEditorLink& link) { return link.to == end_attr; });
|
|
links.push({u32(start_attr) & ~OUTPUT_FLAG, u32(end_attr)});
|
|
|
|
pushUndo(SimpleUndoRedo::NO_MERGE_UNDO);
|
|
}
|
|
}
|
|
|
|
ImGuiEx::EndNodeEditor();
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) {
|
|
if (ImGui::GetIO().KeyAlt && hovered_link != -1) {
|
|
links.erase(hovered_link);
|
|
pushUndo(SimpleUndoRedo::NO_MERGE_UNDO);
|
|
}
|
|
else {
|
|
onCanvasClicked(ImGui::GetMousePos() - origin - m_offset, hovered_link);
|
|
}
|
|
}
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(1)) {
|
|
ImGui::OpenPopup("context_menu");
|
|
m_half_link_start = 0;
|
|
}
|
|
|
|
if (ImGui::BeginPopup("context_menu")) {
|
|
const ImVec2 pos = ImGui::GetMousePosOnOpeningCurrentPopup() - origin - m_offset;
|
|
onContextMenu(pos);
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
m_is_any_item_active = ImGui::IsAnyItemActive();
|
|
|
|
m_canvas.end();
|
|
}
|
|
|
|
RecentPaths::RecentPaths(const char* settings_name, u32 max_paths, StudioApp& app)
|
|
: m_app(app)
|
|
, m_settings_name(settings_name)
|
|
, m_max_paths(max_paths)
|
|
, m_paths(app.getAllocator())
|
|
{
|
|
}
|
|
|
|
void RecentPaths::onBeforeSettingsSaved() {
|
|
Settings& settings = m_app.getSettings();
|
|
for (const String& p : m_paths) {
|
|
const u32 i = u32(&p - m_paths.begin());
|
|
const StaticString<32> key(m_settings_name, i);
|
|
settings.setValue(Settings::LOCAL, key, p.c_str());
|
|
}
|
|
}
|
|
|
|
void RecentPaths::onSettingsLoaded() {
|
|
m_paths.clear();
|
|
char tmp[LUMIX_MAX_PATH];
|
|
Settings& settings = m_app.getSettings();
|
|
FileSystem& fs = m_app.getEngine().getFileSystem();
|
|
for (u32 i = 0; ; ++i) {
|
|
const StaticString<32> key(m_settings_name, i);
|
|
const u32 len = settings.getValue(Settings::LOCAL, key, Span(tmp));
|
|
if (len == 0) break;
|
|
if (fs.fileExists(tmp)) m_paths.emplace(tmp, m_app.getAllocator());
|
|
}}
|
|
|
|
void RecentPaths::push(const char* path) {
|
|
String p(path, m_app.getAllocator());
|
|
m_paths.eraseItems([&](const String& s) { return s == path; });
|
|
m_paths.push(static_cast<String&&>(p));
|
|
if (m_paths.size() > (i32)m_max_paths) {
|
|
m_paths.erase(0);
|
|
}
|
|
}
|
|
|
|
const char* RecentPaths::menu() {
|
|
const char* res = nullptr;
|
|
if (ImGui::BeginMenu("Recent", !m_paths.empty())) {
|
|
for (const String& s : m_paths) {
|
|
if (ImGui::MenuItem(s.c_str())) res = s.c_str();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
return res;
|
|
}
|
|
|
|
|
|
} // namespace Lumix
|