diff --git a/TODO b/TODO index 5163f82..bf5232a 100644 --- a/TODO +++ b/TODO @@ -3,23 +3,28 @@ - 0.1a Copy/paste ref management - 0.1a Disable unavailable classes options - 0.1a Display all available classes options in list view -- Loading character file -Editing character file -- 0.3a Loading skeleton file -- 0.3a Bone select edit -- Animation edit -Behaviour diagnosis -- 0.2a Class hint - 0.1a New file + +- 0.2a Class hint - 0.2a Reindex objects - 0.2a Navigate to specific object in column view - 0.2a Automatic state id distribution -- 0.3a Bone weight/index array - 0.2a Opitimize buildRefList + +- 0.3a Loading skeleton file +- 0.3a Bone select button +- 0.3a Bone weight/index array - 0.3a Edit pointer variable - 0.3a BlenderGen fix -- Ref list update with pointer variables -Display names within the var/state/ref/evt edit when not editing (using combobox?) - 0.3a Set keyboard focus when invoking var/evt select popup -- Macros -- Parse trigger macro \ No newline at end of file + +- 0.4a Animation select button +- 0.4a Loading character file +- 0.4a Ref list update with pointer variables +- 0.4a Macros +- 0.4a Parse trigger macro + +Behaviour diagnosis +Display names within the var/state/ref/evt edit when not editing (using combobox?) +Editing character file +Refactor the shit out of the bloody widgets! \ No newline at end of file diff --git a/cmake/headerlist.cmake b/cmake/headerlist.cmake index 6fede9b..15a61b1 100644 --- a/cmake/headerlist.cmake +++ b/cmake/headerlist.cmake @@ -19,4 +19,5 @@ set(headers src/ui/propedit.h src/ui/columnview.h src/ui/macros.h + src/ui/charedit.h ) diff --git a/cmake/sourcelist.cmake b/cmake/sourcelist.cmake index 512040d..1717f77 100644 --- a/cmake/sourcelist.cmake +++ b/cmake/sourcelist.cmake @@ -16,4 +16,5 @@ set(sources src/ui/propedit.cpp src/ui/columnview.cpp src/ui/macros.cpp + src/ui/charedit.cpp ) \ No newline at end of file diff --git a/src/hkx/hkxfile.cpp b/src/hkx/hkxfile.cpp index 721b6b9..f24078c 100644 --- a/src/hkx/hkxfile.cpp +++ b/src/hkx/hkxfile.cpp @@ -408,7 +408,7 @@ void BehaviourFile::loadFile(std::string_view path) void BehaviourFile::saveFile(std::string_view path) { reindexEvents(); - reindexProperties(); + reindexProps(); reindexVariables(); HkxFile::saveFile(path); @@ -474,7 +474,7 @@ void BehaviourFile::reindexEvents() walker.m_remap = &remap; m_data_node.traverse(walker); } -void BehaviourFile::reindexProperties() +void BehaviourFile::reindexProps() { auto remap = m_prop_manager.reindex(); diff --git a/src/hkx/hkxfile.h b/src/hkx/hkxfile.h index 531fe33..5df59f7 100644 --- a/src/hkx/hkxfile.h +++ b/src/hkx/hkxfile.h @@ -122,7 +122,7 @@ class BehaviourFile : public HkxFile void reindexVariables(); void reindexEvents(); - void reindexProperties(); + void reindexProps(); // removed unreferenced void cleanupVariables(); @@ -139,6 +139,7 @@ class SkeletonFile : public HkxFile public: void loadFile(std::string_view path); + inline pugi::xml_node getBoneNode(bool ragdoll = false) { return (ragdoll ? m_skel_rag_obj : m_skel_obj).getByName("bones"); } void getBoneList(std::vector& out, bool ragdoll = false); inline std::string_view getBone(size_t idx, bool ragdoll = false) { @@ -165,9 +166,8 @@ class CharacterFile : public HkxFile inline pugi::xml_node getAnimNames() { return m_anim_name_node; } -private: VariableManager m_prop_manager; // naming a bit confusing but charprops are essentially variables in character files - +private: pugi::xml_node m_anim_name_node; pugi::xml_node m_char_data_obj, m_char_str_data_obj, m_var_value_obj; }; diff --git a/src/hkx/linkedmanager.h b/src/hkx/linkedmanager.h index 1ee75d7..347aae7 100644 --- a/src/hkx/linkedmanager.h +++ b/src/hkx/linkedmanager.h @@ -11,6 +11,7 @@ #include #include +#include namespace Haviour { @@ -62,6 +63,7 @@ class LinkedPropertyEntry constexpr pugi::xml_node get() { return m_props[getTypeIndex()]; } std::enable_if_t::value, const char*> getName() { return get().text().as_string(); } + std::enable_if_t::value, std::string> getItemName() { return fmt::format("{:4} {}", m_index, getName()); } static LinkedPropertyEntry create(NodeArray& containers) { diff --git a/src/ui/charedit.cpp b/src/ui/charedit.cpp new file mode 100644 index 0000000..ad4fcc8 --- /dev/null +++ b/src/ui/charedit.cpp @@ -0,0 +1,121 @@ +#include "charedit.h" +#include "widgets.h" +#include "hkx/hkclass.inl" + +#include + +namespace Haviour +{ +namespace Ui +{ +CharEdit* CharEdit::getSingleton() +{ + static CharEdit edit; + return std::addressof(edit); +} + +void CharEdit::show() +{ +} + +void CharEdit::showPropList() +{ + ImGui::PushID("propedit"); + constexpr auto table_flag = + ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY | + ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_NoBordersInBody; + + bool scroll_to_bottom = false; + auto file_manager = Hkx::HkxFileManager::getSingleton(); + auto& current_file = file_manager->m_char_file; + + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Character Property"), ImGui::SameLine(); + if (ImGui::Button(ICON_FA_PLUS_CIRCLE)) + ImGui::OpenPopup("Type Select"); + addTooltip("Add new character property"); + if (ImGui::BeginPopup("Type Select")) + { + for (auto data_type : Hkx::e_variableType) + if ((data_type != "VARIABLE_TYPE_INVALID") && ImGui::Selectable(data_type.data())) + { + ImGui::CloseCurrentPopup(); + scroll_to_bottom = true; + current_file.m_prop_manager.addEntry(Hkx::getVarTypeEnum(data_type)); + break; + } + ImGui::EndPopup(); + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_HASHTAG)) + current_file.m_prop_manager.reindex(); + addTooltip("Reindex variables\nDiscard all variables marked obsolete"); + + ImGui::InputText("Filter", &m_prop_filter), ImGui::SameLine(); + ImGui::Button(ICON_FA_QUESTION_CIRCLE); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::TextColored(g_color_invalid, "Invalid"); + ImGui::TextColored(g_color_bool, "Boolean"); + ImGui::TextColored(g_color_int, "Int8/16/32"); + ImGui::TextColored(g_color_float, "Real"); + ImGui::TextColored(g_color_attr, "Pointer"); + ImGui::TextColored(g_color_quad, "Vector/Quaternion"); + ImGui::EndTooltip(); + } + ImGui::Separator(); + + if (ImGui::BeginTable("##PropList", 2, table_flag, ImVec2(-FLT_MIN, -FLT_MIN))) + { + ImGui::TableSetupColumn("id", ImGuiTableColumnFlags_WidthFixed, 36); + ImGui::TableSetupColumn("name", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableNextRow(); + + auto var_list = current_file.m_prop_manager.getEntryList(); + std::erase_if(var_list, + [=](auto& var) { + auto disp_name = std::format("{:3} {}", var.m_index, var.get().text().as_string()); // :3 + return !(var.m_valid && + (m_prop_filter.empty() || + !std::ranges::search(disp_name, m_prop_filter, [](char ch1, char ch2) { return std::toupper(ch1) == std::toupper(ch2); }).empty())); + }); + + ImGuiListClipper clipper; + clipper.Begin(var_list.size()); + while (clipper.Step()) + for (int row_n = clipper.DisplayStart; row_n < clipper.DisplayEnd; row_n++) + { + auto& var = var_list[row_n]; + + ImGui::TableNextColumn(); + + auto var_type = var.get().getByName("type").text().as_string(); + ImGui::PushStyleColor(ImGuiCol_Text, getVarColor(var)); + ImGui::Text("%d", var.m_index); + addTooltip(var_type); + ImGui::PopStyleColor(); + + ImGui::TableNextColumn(); + const bool is_selected = false; + if (ImGui::Selectable(std::format("{}##{}", var.get().text().as_string(), var.m_index).c_str(), is_selected)) + { + m_prop_current = var; + ImGui::OpenPopup("Editing Varibale"); + } + addTooltip("Click to edit"); + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + // varEditPopup("Editing Varibale", m_prop_current, current_file); + + if (scroll_to_bottom) + ImGui::SetScrollHereY(1.0f); + + ImGui::EndTable(); + } + ImGui::PopID(); +} + +} // namespace Ui +} // namespace Haviour \ No newline at end of file diff --git a/src/ui/charedit.h b/src/ui/charedit.h new file mode 100644 index 0000000..5099243 --- /dev/null +++ b/src/ui/charedit.h @@ -0,0 +1,25 @@ +#pragma once + +#include "hkx/linkedmanager.h" + +namespace Haviour +{ +namespace Ui +{ +class CharEdit +{ +public: + static CharEdit* getSingleton(); + + void show(); + + bool m_show = false; + +private: + std::string m_prop_filter = {}; + Hkx::Variable m_prop_current = {}; + + void showPropList(); +}; +} // namespace Ui +} // namespace Haviour \ No newline at end of file diff --git a/src/ui/classinterface.cpp b/src/ui/classinterface.cpp index 06c9680..ecff001 100644 --- a/src/ui/classinterface.cpp +++ b/src/ui/classinterface.cpp @@ -665,8 +665,8 @@ UICLASS(hkbBlenderGenerator) intScalarEdit(obj.getByName("indexOfSyncMasterChild"), file, ImGuiDataType_S16, "If you want a particular child's duration to be used to sync all of the other children, set this to the index of the child.\n" "Otherwise, set it to -1."); - flagEdit(obj.getByName("flags"), Hkx::f_hkbBlenderGenerator_BlenderFlags, - "The flags affecting specialized behavior.", "", true); + flagEdit(obj.getByName("flags"), Hkx::f_hkbBlenderGenerator_BlenderFlags, + "The flags affecting specialized behavior.", ""); boolEdit(obj.getByName("subtractLastChild"), file, "If this is set to true then the last child will be a subtracted from the blend of the rest"); @@ -802,7 +802,6 @@ UICLASS(hkbBoneIndexArray) if (ImGui::InputTextMultiline(bone_indices.attribute("name").as_string(), &value)) bone_indices.text() = value.c_str(); - ImGui::TableNextColumn(); ImGui::EndTable(); @@ -840,7 +839,8 @@ UICLASS(hkbBoneIndexArray) ImGui::TableNextColumn(); ImGui::InputScalar(fmt::format("{}", i).c_str(), ImGuiDataType_S16, &bone_idxs[i]); ImGui::TableNextColumn(); - bonePickerButton(skel_file, bone_idxs[i]); + if (auto res = bonePickerButton("picker", skel_file, bone_idxs[i]); res.has_value()) + bone_idxs[i] = res.value(); ImGui::PopID(); } ImGui::EndTable(); diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index 870d9ab..9b23477 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -234,18 +234,13 @@ void showMenuBar() } if (ImGui::BeginMenu("Resources")) { - if (ImGui::MenuItem("Load Project File")) {} - addTooltip("Load both the character and skeleton"); - - ImGui::Separator(); - - if (ImGui::MenuItem("Load Skeleton")) + if (ImGui::MenuItem("Load Character")) { nfdchar_t* outPath = nullptr; nfdresult_t result = NFD_OpenDialog(nullptr, nullptr, &outPath); if (result == NFD_OKAY) { - file_manager->m_skel_file.loadFile(outPath); + file_manager->m_char_file.loadFile(outPath); free(outPath); } else if (result == NFD_ERROR) @@ -253,13 +248,13 @@ void showMenuBar() spdlog::error("Error with file dialog:\n\t{}", NFD_GetError()); } } - if (ImGui::MenuItem("Load Character")) + if (ImGui::MenuItem("Load Skeleton")) { nfdchar_t* outPath = nullptr; nfdresult_t result = NFD_OpenDialog(nullptr, nullptr, &outPath); if (result == NFD_OKAY) { - file_manager->m_char_file.loadFile(outPath); + file_manager->m_skel_file.loadFile(outPath); free(outPath); } else if (result == NFD_ERROR) @@ -270,8 +265,8 @@ void showMenuBar() ImGui::Separator(); - ImGui::TextDisabled("Skelton: %s", file_manager->m_skel_file.isFileLoaded() ? file_manager->m_skel_file.getPath().data() : "None"); ImGui::TextDisabled("Character: %s", file_manager->m_char_file.isFileLoaded() ? file_manager->m_char_file.getPath().data() : "None"); + ImGui::TextDisabled("Skelton: %s", file_manager->m_skel_file.isFileLoaded() ? file_manager->m_skel_file.getPath().data() : "None"); ImGui::EndMenu(); } diff --git a/src/ui/varedit.cpp b/src/ui/varedit.cpp index 5d02846..d9b3c5a 100644 --- a/src/ui/varedit.cpp +++ b/src/ui/varedit.cpp @@ -274,7 +274,7 @@ void VarEdit::showPropList() addTooltip("Remove unused properties (USE WITH CAUTION!)"); ImGui::SameLine(); if (ImGui::Button(ICON_FA_HASHTAG)) - current_file.reindexProperties(); + current_file.reindexProps(); addTooltip("Reindex properties\nDiscard all events properties obsolete"); ImGui::InputText("Filter", &m_prop_filter); diff --git a/src/ui/widgets.cpp b/src/ui/widgets.cpp index 822077a..bee77eb 100644 --- a/src/ui/widgets.cpp +++ b/src/ui/widgets.cpp @@ -18,102 +18,99 @@ bool iconButton(const char* label) return ImGui::Button(label, ImVec2(10, 10)); } -void statePickerPopup(const char* str_id, pugi::xml_node hkparam, pugi::xml_node state_machine, Hkx::BehaviourFile& file) +std::optional statePickerPopup(const char* str_id, pugi::xml_node state_machine, Hkx::HkxFile& file, int selected_state_id, bool just_open) { - if (ImGui::BeginPopup(str_id, ImGuiWindowFlags_AlwaysAutoResize)) + if (ImGui::IsPopupOpen(str_id)) { - auto states_node = state_machine.getByName("states"); - size_t num_objs = states_node.attribute("numelements").as_ullong(); - std::istringstream states_stream(states_node.text().as_string()); + auto states_node = state_machine.getByName("states"); + size_t num_objs = states_node.attribute("numelements").as_ullong(); + std::istringstream states_stream(states_node.text().as_string()); + std::vector states(num_objs); for (size_t i = 0; i < num_objs; ++i) { std::string temp_str; states_stream >> temp_str; - - auto state = file.getObj(temp_str); - auto state_id = state.getByName("stateId").text().as_int(); - - const bool selected = false; - if (ImGui::Selectable(fmt::format("{:4} {}", state_id, state.getByName("name").text().as_string()).c_str(), selected)) - { - hkparam.text() = state_id; - ImGui::CloseCurrentPopup(); - break; - } + states[i] = file.getObj(temp_str); } - ImGui::EndPopup(); + return pickerPopup( + str_id, states, + [](auto& state_obj, std::string_view filter_str) { + auto state_id = state_obj.getByName("stateId").text().as_int(); + return hasText(fmt::format("{:4} {}", state_id, state_obj.getByName("name").text().as_string()), filter_str); }, + [=](auto& state_obj) { + auto state_id = state_obj.getByName("stateId").text().as_int(); + return ImGui::Selectable(fmt::format("{:4} {}", state_id, state_obj.getByName("name").text().as_string()).c_str(), selected_state_id == state_id); + }, + just_open); } + return std::nullopt; } -std::optional bonePickerButton(Hkx::SkeletonFile& skel_file, int16_t value) +std::optional bonePickerPopup(const char* str_id, Hkx::SkeletonFile& skel_file, int16_t selected_bone_id, bool just_open) { - bool set_focus = false; - if (ImGui::Button(ICON_FA_SEARCH)) - { - ImGui::OpenPopup("select bone"); - set_focus = true; - } - addTooltip("Select"); - - static std::string search_text = {}; - static bool use_ragdoll_skel = false; + static bool use_ragdoll_skel = false; - if (ImGui::BeginPopup("select bone", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar)) + if (ImGui::BeginPopup(str_id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar)) { if (skel_file.isFileLoaded()) { ImGui::Checkbox("Ragdoll Skeleton", &use_ragdoll_skel); - if (set_focus) - ImGui::SetKeyboardFocusHere(); - ImGui::InputText("Filter", &search_text); - - ImGui::Separator(); - - std::vector bone_list; - skel_file.getBoneList(bone_list, use_ragdoll_skel); + if (just_open) + ImGui::SetKeyboardFocusHere(); // set focus to keyboard - if (bone_list.empty()) - ImGui::TextDisabled("No bones."); - else + auto bone_node = skel_file.getBoneNode(use_ragdoll_skel); + std::vector> bone_list(bone_node.attribute("numelements").as_ullong()); + size_t idx = 0; + for (auto bone : bone_node.children()) { - std::erase_if(bone_list, - [=](std::string_view bone) { - return !search_text.empty() && std::ranges::search(bone, search_text, [](char ch1, char ch2) { return std::toupper(ch1) == std::toupper(ch2); }).empty(); - }); - - ImGui::BeginChild("ITEMS", {400.0f, 20.0f * std::min(bone_list.size(), 30ull)}, false); - { - ImGuiListClipper clipper; - clipper.Begin(bone_list.size()); + bone_list[idx] = {idx, bone}; + ++idx; + } - while (clipper.Step()) - for (int row_n = clipper.DisplayStart; row_n < clipper.DisplayEnd; row_n++) - { - auto bone = bone_list[row_n]; - auto bone_disp_name = fmt::format("{:4} {}", row_n, bone); + auto res = filteredPickerListBox>( + fmt::format("##{}", str_id).c_str(), + bone_list, + [=](auto& pair, std::string_view text) { return hasText(pair.second.getByName("name").text().as_string(), text); }, + [=](auto& pair) { return ImGui::Selectable(fmt::format("{:4} {}", pair.first, pair.second.getByName("name").text().as_string()).c_str(), selected_bone_id == pair.first); }); - if (ImGui::Selectable(bone_disp_name.c_str(), value == row_n)) - { - ImGui::CloseCurrentPopup(); - ImGui::EndChild(); - ImGui::EndPopup(); - return row_n; - } - } - } - ImGui::EndChild(); + if (res.has_value()) + { + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + return res.value().first; } } else + { ImGui::TextDisabled("No loaded skeleton file."); + } ImGui::EndPopup(); } return std::nullopt; } +std::optional animPickerPopup(const char* str_id, Hkx::CharacterFile& char_file, std::string_view selected_anim, bool just_open) +{ + if (ImGui::IsPopupOpen(str_id)) + { + auto anim_names_node = char_file.getAnimNames(); + std::vector anim_list(anim_names_node.attribute("numelements").as_ullong()); + size_t idx = 0; + for (auto anim_name : anim_names_node.children()) + anim_list[idx++] = anim_name.text().as_string(); + + return pickerPopup( + str_id, anim_list, + [](auto& name, std::string_view filter_str) { return hasText(name, filter_str); }, + [=](auto& name) { return ImGui::Selectable(name.data(), name == selected_anim); }, + just_open); + } + return std::nullopt; +} + std::optional animPickerButton(Hkx::CharacterFile& char_file, std::string_view value) { bool set_focus = false; @@ -174,7 +171,7 @@ std::optional animPickerButton(Hkx::CharacterFile& char_file, } } else - ImGui::TextDisabled("No loaded skeleton file."); + ImGui::TextDisabled("No loaded character file."); ImGui::EndPopup(); } @@ -311,8 +308,7 @@ void animEdit(pugi::xml_node hkparam, stringEdit(hkparam, hint, manual_name); - auto res = animPickerButton(char_file, hkparam.text().as_string()); - if (res.has_value()) + if (auto res = animPickerButton("picker", char_file, hkparam.text().as_string()); res.has_value()) hkparam.text() = res.value().data(); ImGui::PopID(); @@ -814,27 +810,18 @@ void stateEdit(pugi::xml_node hkparam, std::string_view hint, std::string_view manual_name) { - ImGui::PushID(manual_name.empty() ? hkparam.attribute("name").as_string() : manual_name.data()); - intScalarEdit(hkparam, file, 4, hint, manual_name); if (getParentObj(hkparam).getByName("variableBindingSet")) ImGui::SameLine(); - if (ImGui::Button(ICON_FA_SEARCH)) - ImGui::OpenPopup("select state"); - addTooltip("Select"); - - if (state_machine) + auto res = statePickerButton("select state", state_machine, file, hkparam.text().as_int()); + if (res.has_value()) + hkparam.text() = res.value().getByName("stateId").text().as_string(); + if (auto state = getStateById(state_machine, hkparam.text().as_int(), file); state) { - statePickerPopup("select state", hkparam, state_machine, file); - if (auto state = getStateById(state_machine, hkparam.text().as_int(), file); state) - { - ImGui::SameLine(); - ImGui::TextUnformatted(state.getByName("name").text().as_string()); - } + ImGui::SameLine(); + ImGui::TextUnformatted(state.getByName("name").text().as_string()); } - - ImGui::PopID(); } void boneEdit(pugi::xml_node hkparam, @@ -843,17 +830,13 @@ void boneEdit(pugi::xml_node hkparam, std::string_view hint, std::string_view manual_name) { - ImGui::PushID(manual_name.empty() ? hkparam.attribute("name").as_string() : manual_name.data()); - intScalarEdit(hkparam, file, ImGuiDataType_S16, hint, manual_name); if (getParentObj(hkparam).getByName("variableBindingSet")) ImGui::SameLine(); - auto picked_bone = bonePickerButton(skel_file, hkparam.text().as_int()); + auto picked_bone = bonePickerButton(manual_name.empty() ? hkparam.attribute("name").as_string() : manual_name.data(), skel_file, hkparam.text().as_int()); if (picked_bone.has_value()) hkparam.text() = picked_bone.value(); - - ImGui::PopID(); } //////////////// Linked Prop Edits diff --git a/src/ui/widgets.h b/src/ui/widgets.h index 2bfd4a0..1863d2d 100644 --- a/src/ui/widgets.h +++ b/src/ui/widgets.h @@ -3,6 +3,8 @@ #pragma once #include "hkx/hkxfile.h" +#include + #include #include #include @@ -26,6 +28,23 @@ const auto g_color_float = ImColor(0xF6, 0x6f, 0x9a).Value; const auto g_color_attr = ImColor(0x9d, 0x00, 0x1c).Value; const auto g_color_quad = ImColor(0x5a, 0xe6, 0xb8).Value; +inline ImVec4 getVarColor(Hkx::Variable& var) +{ + auto var_type_enum = Hkx::getVarTypeEnum(var.get().getByName("type").text().as_string()); + if (var_type_enum < 0) + return g_color_invalid; + else if (var_type_enum < 1) + return g_color_bool; + else if (var_type_enum < 4) + return g_color_int; + else if (var_type_enum < 5) + return g_color_float; + else if (var_type_enum < 6) + return g_color_attr; + else + return g_color_quad; +} + #define copyableText(text, ...) \ ImGui::TextUnformatted(text, __VA_ARGS__); \ addTooltip("Left click to copy text"); \ @@ -35,157 +54,175 @@ const auto g_color_quad = ImColor(0x5a, 0xe6, 0xb8).Value; #define addTooltip(...) \ if (ImGui::IsItemHovered()) ImGui::SetTooltip(__VA_ARGS__); -template -std::optional linkedPropPickerPopup(const char* str_id, - Manager& prop_manager, - bool set_focus) +// Listbox with clipping and custom item widget function +// This function returns selected index if selected +// item function should return true if selected +template +std::optional pickerListBox(const char* label, ImVec2 size, std::vector& items, std::function item_func) { - static bool need_update = true; - static std::string search_text; - - if (ImGui::BeginPopup(str_id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar)) + if (ImGui::BeginListBox(label, size)) { - if (set_focus) - ImGui::SetKeyboardFocusHere(); - ImGui::InputText("Filter", &search_text); - - ImGui::Separator(); - - auto prop_list = prop_manager.getEntryList(); - std::erase_if(prop_list, - [=](auto& prop) { - auto prop_disp_name = std::format("{:4} {}", prop.m_index, prop.get().text().as_string()); // :3 - return !(prop.m_valid && - (search_text.empty() || - !std::ranges::search(prop_disp_name, search_text, [](char ch1, char ch2) { return std::toupper(ch1) == std::toupper(ch2); }).empty())); - }); + ImGuiListClipper clipper; + clipper.Begin(items.size()); - ImGui::BeginChild("ITEMS", {400.0f, 20.0f * std::min(prop_list.size(), 30ull)}, false); - { - ImGuiListClipper clipper; - clipper.Begin(prop_list.size()); - while (clipper.Step()) - for (int row_n = clipper.DisplayStart; row_n < clipper.DisplayEnd; row_n++) + while (clipper.Step()) + for (int row_n = clipper.DisplayStart; row_n < clipper.DisplayEnd; row_n++) + { + if (item_func(items[row_n])) { - auto& prop = prop_list[row_n]; - - if (prop.m_valid) - { - auto prop_disp_name = fmt::format("{:4} {}", prop.m_index, prop.get().text().as_string()); - - if constexpr (std::is_same_v) - { - auto var_type = prop.get().getByName("type").text().as_string(); - auto var_type_enum = Hkx::getVarTypeEnum(var_type); - if (var_type_enum < 0) - ImGui::PushStyleColor(ImGuiCol_Text, g_color_invalid); - else if (var_type_enum < 1) - ImGui::PushStyleColor(ImGuiCol_Text, g_color_bool); - else if (var_type_enum < 4) - ImGui::PushStyleColor(ImGuiCol_Text, g_color_int); - else if (var_type_enum < 5) - ImGui::PushStyleColor(ImGuiCol_Text, g_color_float); - else if (var_type_enum < 6) - ImGui::PushStyleColor(ImGuiCol_Text, g_color_attr); - else - ImGui::PushStyleColor(ImGuiCol_Text, g_color_quad); - } - - if (ImGui::Selectable(prop_disp_name.c_str())) - { - if constexpr (std::is_same_v) - ImGui::PopStyleColor(); - ImGui::CloseCurrentPopup(); - ImGui::EndPopup(); - return prop; - } - - if constexpr (std::is_same_v) - ImGui::PopStyleColor(); - } + ImGui::EndListBox(); + return row_n; } - } - ImGui::EndChild(); - - ImGui::EndPopup(); + } + ImGui::EndListBox(); } + return std::nullopt; +} +// pickerListBox + filter +// filter function returns true if item should remain +template +std::optional filteredPickerListBox(const char* label, + std::vector& items, + std::function filter_func, + std::function item_func) +{ + static std::string filter_text = {}; + ImGui::InputText("Filter", &filter_text); + + std::vector items_filtered = {}; + for (auto& item : items) + if (filter_func(item, filter_text)) + items_filtered.push_back(item); + + if (items_filtered.empty()) + ImGui::TextDisabled("No item"); + else if (auto res = pickerListBox(label, {400.0f, 21.0f * std::min(items_filtered.size(), 30ull)}, items_filtered, item_func); res.has_value()) + return items[res.value()]; return std::nullopt; } -template -void flagPopup(const char* str_id, pugi::xml_node hkparam, const std::array& flags) +template +std::optional pickerPopup(const char* str_id, + std::vector& items, + std::function filter_func, + std::function item_func, + bool just_open) { - std::string value_text = hkparam.text().as_string(); - uint32_t value = 0; - if (ImGui::BeginPopup(str_id)) + if (ImGui::BeginPopup(str_id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar)) { - bool value_changed = false; - for (EnumWrapper flag : flags) - { - if (flag.val && value_text.contains(flag.name)) - value |= flag.val; - } - for (EnumWrapper flag : flags) - if (flag.val) - { - if (ImGui::CheckboxFlags(flag.name.data(), &value, flag.val)) - value_changed = true; - if (!flag.hint.empty()) - addTooltip(flag.hint.data()); - } - if (value_changed) - { - value_text = {}; - if (!value) value_text = "0"; - else - { - for (EnumWrapper flag : flags) - if (value & flag.val) - value_text.append(fmt::format("{}|", flag.name)); - if (value_text.ends_with('|')) - value_text.pop_back(); - } - hkparam.text() = value_text.c_str(); - } + if (just_open) + ImGui::SetKeyboardFocusHere(); // set focus to keyboard + auto res = filteredPickerListBox(fmt::format("##{}", str_id).c_str(), items, filter_func, item_func); + if (res.has_value()) + ImGui::CloseCurrentPopup(); ImGui::EndPopup(); + return res; } + return std::nullopt; } -template -void flagIntPopup(const char* str_id, pugi::xml_node hkparam, const std::array& flags) +template +bool linkedPropSelectable(Entry& prop) +{ + if constexpr (std::is_same_v) + ImGui::PushStyleColor(ImGuiCol_Text, getVarColor(prop)); + if (ImGui::Selectable(prop.getItemName().c_str())) + { + if constexpr (std::is_same_v) + ImGui::PopStyleColor(); + return true; + } + if constexpr (std::is_same_v) + ImGui::PopStyleColor(); + return false; +}; + +template +std::optional linkedPropPickerPopup(const char* str_id, Manager& prop_manager, bool just_open) { - uint32_t value = hkparam.text().as_uint(); - if (ImGui::BeginPopup(str_id)) + using Entry = typename Manager::Entry; + + auto entries = prop_manager.getEntryList(); + return pickerPopup( + str_id, entries, + [](Entry& prop, std::string_view filter_str) { return hasText(prop.getItemName(), filter_str); }, + linkedPropSelectable, + just_open); +} + +// very ugly, but templates just don't fucking works +#define pickerButton(func, ...) \ + ImGui::PushID(str_id); \ + bool open = false; \ + if (ImGui::Button(ICON_FA_SEARCH)) \ + { \ + open = true; \ + ImGui::OpenPopup("select prop"); \ + } \ + auto res = func("select prop", __VA_ARGS__, open); \ + ImGui::PopID(); \ + return res; + +template +std::optional linkedPropPickerButton(const char* str_id, Manager& prop_manager) +{ + pickerButton(linkedPropPickerPopup, prop_manager); +} + +template +void flagEditButton(const char* str_id, pugi::xml_node hkparam, const std::array& flags) +{ + ImGui::PushID(str_id); + + if (ImGui::Button(ICON_FA_FLAG)) + ImGui::OpenPopup("edit flags"); + + if (ImGui::BeginPopup("edit flags")) { - for (EnumWrapper flag : flags) + uint32_t value; + + if constexpr (as_int) + value = hkparam.text().as_uint(); + else + value = str2FlagVal(hkparam.text().as_string(), flags); + + for (auto& flag : flags) if (flag.val) { - ImGui::CheckboxFlags(flag.name.data(), &value, flag.val); + if (ImGui::CheckboxFlags(flag.name.data(), &value, flag.val)) + { + if constexpr (as_int) + hkparam.text() = value; + else + hkparam.text() = flagVal2Str(value, flags).c_str(); + } if (!flag.hint.empty()) addTooltip(flag.hint.data()); } - hkparam.text() = value; ImGui::EndPopup(); } + + ImGui::PopID(); } -template -void flagEditButton(const char* str_id, pugi::xml_node hkparam, const std::array& flags, bool as_int = false) +std::optional statePickerPopup(const char* str_id, pugi::xml_node state_machine, Hkx::HkxFile& file, int selected_state_id, bool just_open); +inline std::optional statePickerButton(const char* str_id, pugi::xml_node state_machine, Hkx::HkxFile& file, int selected_state_id) { - if (as_int) - flagIntPopup(str_id, hkparam, flags); - else - flagPopup(str_id, hkparam, flags); - if (ImGui::Button(ICON_FA_FLAG)) - ImGui::OpenPopup(str_id); - addTooltip("Edit individual flags"); + pickerButton(statePickerPopup, state_machine, file, selected_state_id); } -void statePickerPopup(const char* str_id, pugi::xml_node hkparam, pugi::xml_node state_machine, Hkx::BehaviourFile& file); - -std::optional bonePickerButton(Hkx::SkeletonFile& skel_file, int16_t value); +std::optional bonePickerPopup(const char* str_id, Hkx::SkeletonFile& skel_file, int16_t selected_bone_id, bool just_open); +inline std::optional bonePickerButton(const char* str_id, Hkx::SkeletonFile& skel_file, int16_t selected_bone_id) +{ + pickerButton(bonePickerPopup, skel_file, selected_bone_id); +} +std::optional animPickerPopup(const char* str_id, Hkx::CharacterFile& char_file, std::string_view selected_anim, bool just_open); +inline std::optional animPickerButton(const char* str_id, Hkx::CharacterFile& char_file, std::string_view selected_anim) +{ + pickerButton(animPickerPopup, char_file, selected_anim); +} std::optional animPickerButton(Hkx::CharacterFile& char_file, std::string_view value); void varBindingButton(const char* str_id, pugi::xml_node hkparam, Hkx::BehaviourFile& file); @@ -250,14 +287,17 @@ void intScalarEdit(pugi::xml_node hkparam, Hkx::BehaviourFile& file, ImGuiDataTy // std::string_view hint = {}, // std::string_view manual_name = {}); -template -void flagEdit(pugi::xml_node hkparam, const std::array& flags, std::string_view hint = {}, std::string_view manual_name = {}, bool as_int = false) +template +void flagEdit(pugi::xml_node hkparam, const std::array& flags, std::string_view hint = {}, std::string_view manual_name = {}) { stringEdit(hkparam, hint, manual_name.empty() ? hkparam.attribute("name").as_string() : manual_name.data()); + flagEditButton("Edit flags", hkparam, flags); +} - ImGui::PushID(manual_name.empty() ? hkparam.attribute("name").as_string() : manual_name.data()); - flagEditButton("Edit flags", hkparam, flags, as_int); - ImGui::PopID(); +template +void flagEdit(pugi::xml_node hkparam, const std::array& flags, std::string_view hint = {}, std::string_view manual_name = {}) +{ + flagEdit(hkparam, flags, hint, manual_name); } template @@ -270,27 +310,9 @@ void linkedPropPickerEdit(pugi::xml_node hkparam, intScalarEdit(hkparam, file, ImGuiDataType_S32, hint, manual_name); if (getParentObj(hkparam).getByName("variableBindingSet")) ImGui::SameLine(); - - ImGui::PushID(manual_name.empty() ? hkparam.attribute("name").as_string() : manual_name.data()); - - bool set_focus = false; - if (ImGui::Button(ICON_FA_SEARCH)) - { - set_focus = true; - ImGui::OpenPopup("PickProp"); - } - addTooltip("Select"); - if (hkparam.text().as_int() >= 0) - { - ImGui::SameLine(); - ImGui::TextUnformatted(prop_manager.getEntry(hkparam.text().as_ullong()).getName()); - } - - // POPUP - if (auto out_prop = linkedPropPickerPopup("PickProp", prop_manager, set_focus); out_prop.has_value()) - hkparam.text() = out_prop.value().m_index; - - ImGui::PopID(); + auto res = linkedPropPickerButton(manual_name.empty() ? hkparam.attribute("name").as_string() : manual_name.data(), prop_manager); + if (res.has_value()) + hkparam.text() = res.value().m_index; } void sliderFloatEdit(pugi::xml_node hkparam, diff --git a/src/utils.h b/src/utils.h index edd7af0..d229adc 100644 --- a/src/utils.h +++ b/src/utils.h @@ -16,6 +16,39 @@ struct EnumWrapper uint32_t val = 0; }; +template +uint32_t str2FlagVal(std::string_view str, const std::array& flags) +{ + uint32_t retval = 0; + for (auto flag : flags) + if (flag.val && str.contains(flag.name)) + retval |= flag.val; + return retval; +} + +template +std::string flagVal2Str(uint32_t value, const std::array& flags) +{ + std::string value_text = {}; + for (auto flag : flags) + if (value & flag.val) + value_text = fmt::format("{}{}|", value_text, flag.name); + if (value_text.ends_with('|')) + value_text.pop_back(); + return value_text; +} + +// filtering +inline bool strCaseContains(std::string_view str, std::string_view target) +{ + return !std::ranges::search(str, target, [](char ch1, char ch2) { return std::toupper(ch1) == std::toupper(ch2); }).empty(); +} + +inline bool hasText(std::string_view str, std::string_view filter_str) +{ + return filter_str.empty() || strCaseContains(str, filter_str); +} + // get index of template argument // source: https://stackoverflow.com/questions/15014096/c-index-of-type-during-variadic-template-expansion template