diff --git a/.Icon.ico-autosave.kra b/.Icon.ico-autosave.kra new file mode 100644 index 0000000..fadfcfb Binary files /dev/null and b/.Icon.ico-autosave.kra differ diff --git a/.clang-format b/.clang-format index faa0e23..035c2f8 100644 --- a/.clang-format +++ b/.clang-format @@ -1,7 +1,7 @@ ColumnLimit: 120 PointerAlignment: Left ReferenceAlignment: Left -AllowShortFunctionsOnASingleLine: None +AllowShortFunctionsOnASingleLine: All AllowShortIfStatementsOnASingleLine: true CommentPragmas: '^' BreakBeforeBraces: Allman diff --git a/CMakeLists.txt b/CMakeLists.txt index dd30ade..1781c6d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,7 +102,7 @@ else () target_compile_options(${PROJECT_NAME} PRIVATE -O0 -pg) else () set(CMAKE_BUILD_TYPE "Release") - target_compile_options(${PROJECT_NAME} PRIVATE -O2) + target_compile_options(${PROJECT_NAME} PRIVATE -Os) endif () target_link_libraries(${PROJECT_NAME} PRIVATE m) @@ -135,4 +135,25 @@ target_link_libraries(${PROJECT_NAME} PRIVATE GL SDL3-static SDL3_mixer::SDL3_mi message(STATUS "System: ${CMAKE_SYSTEM_NAME}") message(STATUS "Project: ${PROJECT_NAME}") message(STATUS "Compiler: ${CMAKE_CXX_COMPILER}") + +get_target_property(PROJECT_COMPILE_OPTIONS ${PROJECT_NAME} COMPILE_OPTIONS) +if (NOT PROJECT_COMPILE_OPTIONS) + set(PROJECT_COMPILE_OPTIONS "") +endif () + +string(TOUPPER "${CMAKE_BUILD_TYPE}" BUILD_TYPE_UPPER) +set(EFFECTIVE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +if (BUILD_TYPE_UPPER) + set(CONFIG_FLAGS_VAR "CMAKE_CXX_FLAGS_${BUILD_TYPE_UPPER}") + if (DEFINED ${CONFIG_FLAGS_VAR}) + string(APPEND EFFECTIVE_CXX_FLAGS " ${${CONFIG_FLAGS_VAR}}") + endif () +endif () +string(STRIP "${EFFECTIVE_CXX_FLAGS}" EFFECTIVE_CXX_FLAGS) +if (EFFECTIVE_CXX_FLAGS STREQUAL "") + set(EFFECTIVE_CXX_FLAGS "") +endif () + +message(STATUS "Compiler Flags: ${EFFECTIVE_CXX_FLAGS}") +message(STATUS "Target Compile Options: ${PROJECT_COMPILE_OPTIONS}") message(STATUS "Build: ${CMAKE_BUILD_TYPE}") diff --git a/src/anm2/animation.cpp b/src/anm2/animation.cpp index 656a505..61d2bb6 100644 --- a/src/anm2/animation.cpp +++ b/src/anm2/animation.cpp @@ -105,10 +105,7 @@ namespace anm2ed::anm2 return element; } - void Animation::serialize(XMLDocument& document, XMLElement* parent) - { - parent->InsertEndChild(to_element(document)); - } + void Animation::serialize(XMLDocument& document, XMLElement* parent) { parent->InsertEndChild(to_element(document)); } std::string Animation::to_string() { @@ -153,13 +150,13 @@ namespace anm2ed::anm2 if (isRootTransform) { - auto root = rootAnimation.frame_generate(t, anm2::ROOT); + auto root = rootAnimation.frame_generate(t, ROOT); transform *= math::quad_model_parent_get(root.position, {}, math::percent_to_unit(root.scale), root.rotation); } for (auto& [id, layerAnimation] : layerAnimations) { - auto frame = layerAnimation.frame_generate(t, anm2::LAYER); + auto frame = layerAnimation.frame_generate(t, LAYER); if (frame.size == vec2() || !frame.isVisible) continue; diff --git a/src/anm2/anm2_animations.cpp b/src/anm2/anm2_animations.cpp index c4a5107..11762d9 100644 --- a/src/anm2/anm2_animations.cpp +++ b/src/anm2/anm2_animations.cpp @@ -131,6 +131,8 @@ namespace anm2ed::anm2 finalIndex -= numDeletedBefore; } + animation.frameNum = animation.length(); + return finalIndex; } diff --git a/src/anm2/anm2_type.h b/src/anm2/anm2_type.h new file mode 100644 index 0000000..e5d15d6 --- /dev/null +++ b/src/anm2/anm2_type.h @@ -0,0 +1,84 @@ +#pragma once + +#include "icon.h" + +#include +#include +#include + +namespace anm2ed::anm2 +{ + constexpr auto ROOT_COLOR = glm::vec4(0.140f, 0.310f, 0.560f, 1.000f); + constexpr auto ROOT_COLOR_ACTIVE = glm::vec4(0.240f, 0.520f, 0.880f, 1.000f); + constexpr auto ROOT_COLOR_HOVERED = glm::vec4(0.320f, 0.640f, 1.000f, 1.000f); + + constexpr auto LAYER_COLOR = glm::vec4(0.640f, 0.320f, 0.110f, 1.000f); + constexpr auto LAYER_COLOR_ACTIVE = glm::vec4(0.840f, 0.450f, 0.170f, 1.000f); + constexpr auto LAYER_COLOR_HOVERED = glm::vec4(0.960f, 0.560f, 0.240f, 1.000f); + + constexpr auto NULL_COLOR = glm::vec4(0.140f, 0.430f, 0.200f, 1.000f); + constexpr auto NULL_COLOR_ACTIVE = glm::vec4(0.250f, 0.650f, 0.350f, 1.000f); + constexpr auto NULL_COLOR_HOVERED = glm::vec4(0.350f, 0.800f, 0.480f, 1.000f); + + constexpr auto TRIGGER_COLOR = glm::vec4(0.620f, 0.150f, 0.260f, 1.000f); + constexpr auto TRIGGER_COLOR_ACTIVE = glm::vec4(0.820f, 0.250f, 0.380f, 1.000f); + constexpr auto TRIGGER_COLOR_HOVERED = glm::vec4(0.950f, 0.330f, 0.490f, 1.000f); + +#define TYPE_LIST \ + X(NONE, "", "", resource::icon::NONE, glm::vec4(), glm::vec4(), glm::vec4()) \ + X(ROOT, "Root", "RootAnimation", resource::icon::ROOT, ROOT_COLOR, ROOT_COLOR_ACTIVE, ROOT_COLOR_HOVERED) \ + X(LAYER, "Layer", "LayerAnimation", resource::icon::LAYER, LAYER_COLOR, LAYER_COLOR_ACTIVE, LAYER_COLOR_HOVERED) \ + X(NULL_, "Null", "NullAnimation", resource::icon::NULL_, NULL_COLOR, NULL_COLOR_ACTIVE, NULL_COLOR_HOVERED) \ + X(TRIGGER, "Triggers", "Triggers", resource::icon::TRIGGERS, TRIGGER_COLOR, TRIGGER_COLOR_ACTIVE, \ + TRIGGER_COLOR_HOVERED) + + enum Type + { +#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) symbol, + TYPE_LIST +#undef X + }; + + constexpr const char* TYPE_STRINGS[] = { +#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) string, + TYPE_LIST +#undef X + }; + + constexpr const char* TYPE_ITEM_STRINGS[] = { +#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) itemString, + TYPE_LIST +#undef X + }; + + constexpr resource::icon::Type TYPE_ICONS[] = { +#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) icon, + TYPE_LIST +#undef X + }; + + constexpr glm::vec4 TYPE_COLOR[] = { +#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) color, + TYPE_LIST +#undef X + }; + + constexpr glm::vec4 TYPE_COLOR_ACTIVE[] = { +#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorActive, + TYPE_LIST +#undef X + }; + + constexpr glm::vec4 TYPE_COLOR_HOVERED[] = { +#define X(symbol, string, itemString, icon, color, colorActive, colorHovered) colorHovered, + TYPE_LIST +#undef X + }; + + enum ChangeType + { + ADD, + SUBTRACT, + ADJUST + }; +} \ No newline at end of file diff --git a/src/anm2/frame.cpp b/src/anm2/frame.cpp index 5e22172..746a308 100644 --- a/src/anm2/frame.cpp +++ b/src/anm2/frame.cpp @@ -114,19 +114,13 @@ namespace anm2ed::anm2 return xml::document_to_string(document); } - void Frame::shorten() - { - delay = glm::clamp(--delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); - } + void Frame::shorten() { delay = glm::clamp(--delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); } - void Frame::extend() - { - delay = glm::clamp(++delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); - } + void Frame::extend() { delay = glm::clamp(++delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX); } bool Frame::is_visible(Type type) { - if (type == anm2::TRIGGER) + if (type == TRIGGER) return isVisible && eventID > -1; else return isVisible; diff --git a/src/anm2/frame.h b/src/anm2/frame.h index 0e5648b..f17a086 100644 --- a/src/anm2/frame.h +++ b/src/anm2/frame.h @@ -4,10 +4,7 @@ #include #include -#include -#include -#include - +#include "anm2_type.h" #include "types.h" namespace anm2ed::anm2 @@ -15,39 +12,6 @@ namespace anm2ed::anm2 constexpr auto FRAME_DELAY_MIN = 1; constexpr auto FRAME_DELAY_MAX = 100000; -#define TYPE_LIST \ - X(NONE, "None", "None") \ - X(ROOT, "Root", "RootAnimation") \ - X(LAYER, "Layer", "LayerAnimation") \ - X(NULL_, "Null", "NullAnimation") \ - X(TRIGGER, "Trigger", "Triggers") - - enum Type - { -#define X(symbol, string, animationString) symbol, - TYPE_LIST -#undef X - }; - - constexpr const char* TYPE_STRINGS[] = { -#define X(symbol, string, animationString) string, - TYPE_LIST -#undef X - }; - - constexpr const char* TYPE_ANIMATION_STRINGS[] = { -#define X(symbol, string, animationString) animationString, - TYPE_LIST -#undef X - }; - - enum ChangeType - { - ADD, - SUBTRACT, - ADJUST - }; - #define MEMBERS \ X(isVisible, bool, true) \ X(isInterpolated, bool, false) \ diff --git a/src/anm2/item.cpp b/src/anm2/item.cpp index d262bf4..c569091 100644 --- a/src/anm2/item.cpp +++ b/src/anm2/item.cpp @@ -24,7 +24,7 @@ namespace anm2ed::anm2 XMLElement* Item::to_element(XMLDocument& document, Type type, int id) { - auto element = document.NewElement(TYPE_ANIMATION_STRINGS[type]); + auto element = document.NewElement(TYPE_ITEM_STRINGS[type]); if (type == LAYER) element->SetAttribute("LayerId", id); if (type == NULL_) element->SetAttribute("NullId", id); diff --git a/src/dialog.cpp b/src/dialog.cpp index ddffb95..c46d064 100644 --- a/src/dialog.cpp +++ b/src/dialog.cpp @@ -2,6 +2,9 @@ #ifdef _WIN32 #include +#elif __unix__ +#else + #include "toast.h" #endif #include @@ -57,8 +60,10 @@ namespace anm2ed { #ifdef _WIN32 ShellExecuteA(NULL, "open", path.c_str(), NULL, NULL, SW_SHOWNORMAL); -#else +#elif __unix__ system(std::format("xdg-open \"{}\" &", path).c_str()); +#else + toasts.info("Operation not supported."); #endif } diff --git a/src/dialog.h b/src/dialog.h index 46f716a..dfd214e 100644 --- a/src/dialog.h +++ b/src/dialog.h @@ -9,9 +9,7 @@ namespace anm2ed::dialog #if defined(_WIN32) #define EXECUTABLE_FILTER {"Executable", "exe"} #else - #define EXECUTABLE_FILTER \ - { \ - } + #define EXECUTABLE_FILTER {"Executable", "*"} #endif #define FILTER_LIST \ diff --git a/src/imgui/documents.cpp b/src/imgui/documents.cpp index 9988778..49b611e 100644 --- a/src/imgui/documents.cpp +++ b/src/imgui/documents.cpp @@ -74,12 +74,9 @@ namespace anm2ed::imgui } auto isRequested = i == manager.pendingSelected; - auto font = isDirty ? font::ITALICS : font::REGULAR; - auto string = isDirty ? std::format("[Not Saved] {}", document.filename_get().string()) : document.filename_get().string(); - auto label = std::format("{}###Document{}", string, i); auto flags = isDirty ? ImGuiTabItemFlags_UnsavedDocument : 0; @@ -89,7 +86,9 @@ namespace anm2ed::imgui if (ImGui::BeginTabItem(label.c_str(), &document.isOpen, flags)) { manager.set(i); + if (isRequested) manager.pendingSelected = -1; + ImGui::EndTabItem(); } ImGui::PopFont(); diff --git a/src/imgui/imgui_.cpp b/src/imgui/imgui_.cpp index 0123551..c761f03 100644 --- a/src/imgui/imgui_.cpp +++ b/src/imgui/imgui_.cpp @@ -166,10 +166,7 @@ namespace anm2ed::imgui return (width - (ImGui::GetStyle().ItemSpacing.x * (float)(count - 1))) / (float)count; } - ImVec2 widget_size_with_row_get(int count, float width) - { - return ImVec2(row_widget_width_get(count, width), 0); - } + ImVec2 widget_size_with_row_get(int count, float width) { return ImVec2(row_widget_width_get(count, width), 0); } float footer_height_get(int itemCount) { @@ -265,17 +262,13 @@ namespace anm2ed::imgui return ImGui::Shortcut(string_to_chord(string), flags); } - MultiSelectStorage::MultiSelectStorage() - { - internal.AdapterSetItemSelected = external_storage_set; - } + MultiSelectStorage::MultiSelectStorage() { internal.AdapterSetItemSelected = external_storage_set; } - void MultiSelectStorage::start(size_t size) + void MultiSelectStorage::start(size_t size, ImGuiMultiSelectFlags flags) { internal.UserData = this; - auto io = ImGui::BeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect2d, - this->size(), size); + auto io = ImGui::BeginMultiSelect(flags, this->size(), size); internal.ApplyRequests(io); } @@ -299,10 +292,7 @@ namespace anm2ed::imgui isJustOpened = true; } - bool PopupHelper::is_open() - { - return isOpen; - } + bool PopupHelper::is_open() { return isOpen; } void PopupHelper::trigger() { @@ -322,13 +312,7 @@ namespace anm2ed::imgui ImGui::SetNextWindowSize(ImVec2(viewport->Size.x * POPUP_MULTIPLIERS[type], 0)); } - void PopupHelper::end() - { - isJustOpened = false; - } + void PopupHelper::end() { isJustOpened = false; } - void PopupHelper::close() - { - isOpen = false; - } + void PopupHelper::close() { isOpen = false; } } diff --git a/src/imgui/imgui_.h b/src/imgui/imgui_.h index 4ff685d..cef732f 100644 --- a/src/imgui/imgui_.h +++ b/src/imgui/imgui_.h @@ -190,7 +190,9 @@ namespace anm2ed::imgui using std::set::erase; MultiSelectStorage(); - void start(size_t); + void start(size_t, ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_BoxSelect2d | + ImGuiMultiSelectFlags_ClearOnEscape | + ImGuiMultiSelectFlags_ScopeWindow); void finish(); }; diff --git a/src/imgui/taskbar.cpp b/src/imgui/taskbar.cpp index 8d263ae..7d624d4 100644 --- a/src/imgui/taskbar.cpp +++ b/src/imgui/taskbar.cpp @@ -1,13 +1,23 @@ #include "taskbar.h" -#include +#include +#include +#include +#include +#include +#include #include +#include + #include "math_.h" #include "render.h" #include "shader.h" +#include "toast.h" #include "types.h" +#include "icon.h" + using namespace anm2ed::resource; using namespace anm2ed::types; using namespace anm2ed::canvas; @@ -16,10 +26,111 @@ using namespace glm; namespace anm2ed::imgui { - Taskbar::Taskbar() : generate(vec2()) +#ifdef __unix__ + + namespace { + constexpr std::array ICON_SIZES{16, 24, 32, 48, 64, 128, 256}; + + bool ensure_parent_directory_exists(const std::filesystem::path& path) + { + std::error_code ec; + std::filesystem::create_directories(path.parent_path(), ec); + if (ec) + { + toasts.warning(std::format("Could not create directory for {} ({})", path.string(), ec.message())); + return false; + } + return true; + } + + bool write_binary_blob(const std::filesystem::path& path, const std::uint8_t* data, size_t size) + { + if (!ensure_parent_directory_exists(path)) return false; + + std::ofstream file(path, std::ios::binary | std::ios::trunc); + if (!file.is_open()) + { + toasts.warning(std::format("Could not open {} for writing", path.string())); + return false; + } + + file.write(reinterpret_cast(data), static_cast(size)); + return true; + } + + bool run_command_checked(const std::string& command, const std::string& description) + { + auto result = std::system(command.c_str()); + if (result != 0) + { + toasts.warning(std::format("{} failed (exit code {})", description, result)); + return false; + } + return true; + } + + bool install_icon_set(const std::string& context, const std::string& iconName, const std::filesystem::path& path) + { + bool success = true; + for (auto size : ICON_SIZES) + { + auto command = std::format("xdg-icon-resource install --noupdate --novendor --context {} --size {} \"{}\" {}", + context, size, path.string(), iconName); + success &= run_command_checked(command, std::format("Install {} icon ({}px)", iconName, size)); + } + return success; + } + + bool uninstall_icon_set(const std::string& context, const std::string& iconName) + { + bool success = true; + for (auto size : ICON_SIZES) + { + auto command = + std::format("xdg-icon-resource uninstall --noupdate --context {} --size {} {}", context, size, iconName); + success &= run_command_checked(command, std::format("Remove {} icon ({}px)", iconName, size)); + } + return success; + } + + bool remove_file_if_exists(const std::filesystem::path& path) + { + std::error_code ec; + if (!std::filesystem::exists(path, ec)) return true; + std::filesystem::remove(path, ec); + if (ec) + { + toasts.warning(std::format("Could not remove {} ({})", path.string(), ec.message())); + return false; + } + return true; + } } + constexpr auto MIME_TYPE = R"( + + + Anm2 Animation + + +)"; + + constexpr auto DESKTOP_ENTRY_FORMAT = R"([Desktop Entry] +Type=Application +Name=Anm2Ed +Icon=anm2ed +Comment=Animation editor for .anm2 files +Exec={} +Terminal=false +Categories=Graphics;Development; +MimeType=application/x-anm2+xml; +)"; + +#endif + + Taskbar::Taskbar() : generate(vec2()) {} + void Taskbar::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, bool& isQuitting) { auto document = manager.get(); @@ -119,6 +230,141 @@ namespace anm2ed::imgui configurePopup.open(); } + ImGui::Separator(); + + if (ImGui::MenuItem("Associate .anm2 Files with Editor", nullptr, false, + !isAnm2Association || !isAbleToAssociateAnm2)) + { +#ifdef _WIN32 + +#elif __unix__ + auto cache_icons = []() + { + auto programIconPath = std::filesystem::path(filesystem::path_icon_get()); + auto fileIconPath = std::filesystem::path(filesystem::path_icon_file_get()); + auto iconBytes = std::size(resource::icon::PROGRAM); + + bool isSuccess = write_binary_blob(programIconPath, resource::icon::PROGRAM, iconBytes) && + write_binary_blob(fileIconPath, resource::icon::PROGRAM, iconBytes); + + if (isSuccess) + { + isSuccess = install_icon_set("apps", "anm2ed", programIconPath) && + install_icon_set("mimetypes", "application-x-anm2+xml", fileIconPath) && + run_command_checked("xdg-icon-resource forceupdate --theme hicolor", "Refresh icon cache"); + } + + remove_file_if_exists(programIconPath); + remove_file_if_exists(fileIconPath); + + if (isSuccess) toasts.info("Cached program and file icons."); + return isSuccess; + }; + + auto register_mime = []() + { + auto path = std::filesystem::path(filesystem::path_mime_get()); + if (!ensure_parent_directory_exists(path)) return false; + + std::ofstream file(path, std::ofstream::out | std::ofstream::trunc); + if (!file.is_open()) + { + toasts.warning(std::format("Could not write .anm2 MIME type: {}", path.string())); + return false; + } + + file << MIME_TYPE; + file.close(); + toasts.info(std::format("Wrote .anm2 MIME type to: {}", path.string())); + + auto mimeRoot = path.parent_path().parent_path(); + auto command = std::format("update-mime-database \"{}\"", mimeRoot.string()); + return run_command_checked(command, "Update MIME database"); + }; + + auto register_desktop_entry = []() + { + auto path = std::filesystem::path(filesystem::path_application_get()); + if (!ensure_parent_directory_exists(path)) return false; + + std::ofstream file(path, std::ofstream::out | std::ofstream::trunc); + if (!file.is_open()) + { + toasts.warning(std::format("Could not write desktop entry: {}", path.string())); + return false; + } + + auto desktopEntry = std::format(DESKTOP_ENTRY_FORMAT, filesystem::path_executable_get()); + file << desktopEntry; + file.close(); + toasts.info(std::format("Wrote desktop entry to: {}", path.string())); + + auto desktopDir = path.parent_path(); + auto desktopUpdate = + std::format("update-desktop-database \"{}\"", desktopDir.empty() ? "." : desktopDir.string()); + auto desktopFileName = path.filename().string(); + auto setDefault = std::format("xdg-mime default {} application/x-anm2+xml", + desktopFileName.empty() ? path.string() : desktopFileName); + + auto databaseUpdated = run_command_checked(desktopUpdate, "Update desktop database"); + auto defaultRegistered = run_command_checked(setDefault, "Set default handler for .anm2"); + return databaseUpdated && defaultRegistered; + }; + + auto iconsCached = cache_icons(); + auto mimeRegistered = register_mime(); + auto desktopRegistered = register_desktop_entry(); + + isAnm2Association = iconsCached && mimeRegistered && desktopRegistered; + if (isAnm2Association) + toasts.info("Associated .anm2 files with the editor."); + else + toasts.warning("Association incomplete. Please review the warnings above."); +#endif + } + ImGui::SetItemTooltip( + "Associate .anm2 files with the application (i.e., clicking on them in a file explorer will " + "open the application)."); + + if (ImGui::MenuItem("Remove .anm2 File Association", nullptr, false, + isAnm2Association || !isAbleToAssociateAnm2)) + { +#ifdef _WIN32 + +#elif __unix__ + { + auto iconsRemoved = + uninstall_icon_set("apps", "anm2ed") && uninstall_icon_set("mimetypes", "application-x-anm2+xml") && + run_command_checked("xdg-icon-resource forceupdate --theme hicolor", "Refresh icon cache"); + if (iconsRemoved) + toasts.info("Removed cached icons."); + else + toasts.warning("Could not remove all cached icons."); + } + + { + auto path = std::filesystem::path(filesystem::path_mime_get()); + auto removed = remove_file_if_exists(path); + if (removed) toasts.info(std::format("Removed .anm2 MIME type: {}", path.string())); + + auto mimeRoot = path.parent_path().parent_path(); + run_command_checked(std::format("update-mime-database \"{}\"", mimeRoot.string()), "Update MIME database"); + } + + { + auto path = std::filesystem::path(filesystem::path_application_get()); + if (remove_file_if_exists(path)) toasts.info(std::format("Removed desktop entry: {}", path.string())); + + auto desktopDir = path.parent_path(); + run_command_checked( + std::format("update-desktop-database \"{}\"", desktopDir.empty() ? "." : desktopDir.string()), + "Update desktop database"); + } +#endif + isAnm2Association = false; + } + ImGui::SetItemTooltip("Unassociate .anm2 files with the application."); + ImGui::EndMenu(); } @@ -546,7 +792,7 @@ namespace anm2ed::imgui if (dialogType == dialog::PNG_DIRECTORY_SET) dialog.folder_open(dialogType); else - dialog.file_open(dialogType); + dialog.file_save(dialogType); } ImGui::SameLine(); input_text_string(type == render::PNGS ? "Directory" : "Path", &path); @@ -581,11 +827,16 @@ namespace anm2ed::imgui ImGui::SameLine(); ImGui::Checkbox("Raw", &isRaw); - ImGui::SetItemTooltip("Record only the layers of the animation."); + ImGui::SetItemTooltip("Record only the raw animation; i.e., only its layers, to its bounds."); + + ImGui::SameLine(); + + ImGui::Checkbox("Sound", &settings.timelineIsSound); + ImGui::SetItemTooltip("Toggle sounds playing with triggers.\nBind sounds to events in the Events window.\nThe " + "output animation will use the played sounds."); if (ImGui::Button("Render", widgetSize)) { - manager.isRecording = true; manager.isRecordingStart = true; playback.time = start; playback.isPlaying = true; diff --git a/src/imgui/taskbar.h b/src/imgui/taskbar.h index 1785468..778683c 100644 --- a/src/imgui/taskbar.h +++ b/src/imgui/taskbar.h @@ -2,6 +2,7 @@ #include "canvas.h" #include "dialog.h" +#include "filesystem_.h" #include "imgui_.h" #include "manager.h" #include "resources.h" @@ -20,6 +21,15 @@ namespace anm2ed::imgui PopupHelper aboutPopup{PopupHelper("About")}; Settings editSettings{}; int selectedShortcut{-1}; + +#if defined(_WIN32) || defined(__unix__) + bool isAbleToAssociateAnm2 = true; +#else + bool isAbleToAssociateAnm2 = false; +#endif + + bool isAnm2Association = std::filesystem::exists(util::filesystem::path_application_get()); + bool isQuittingMode{}; public: diff --git a/src/imgui/toast.cpp b/src/imgui/toast.cpp index 8de56ec..76196d9 100644 --- a/src/imgui/toast.cpp +++ b/src/imgui/toast.cpp @@ -9,7 +9,8 @@ using namespace anm2ed::types; namespace anm2ed::imgui { - constexpr auto LIFETIME = 3.0f; + constexpr auto LIFETIME = 4.0f; + constexpr auto FADE_THRESHOLD = 1.0f; Toast::Toast(const std::string& message) { @@ -30,8 +31,6 @@ namespace anm2ed::imgui { Toast& toast = toasts[i]; - toast.lifetime -= ImGui::GetIO().DeltaTime; - if (toast.lifetime <= 0.0f) { toasts.erase(toasts.begin() + i); @@ -39,7 +38,9 @@ namespace anm2ed::imgui continue; } - auto alpha = toast.lifetime / LIFETIME; + toast.lifetime -= ImGui::GetIO().DeltaTime; + + auto alpha = toast.lifetime <= FADE_THRESHOLD ? toast.lifetime / FADE_THRESHOLD : 1.0f; borderColor.w = alpha; textColor.w = alpha; @@ -57,6 +58,8 @@ namespace anm2ed::imgui { ImGui::TextUnformatted(toast.message.c_str()); position.y -= ImGui::GetWindowSize().y + ImGui::GetStyle().ItemSpacing.y; + + if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) toast.lifetime = 0.0f; } ImGui::End(); ImGui::PopStyleColor(2); diff --git a/src/imgui/window/animation_preview.cpp b/src/imgui/window/animation_preview.cpp index f798abf..5960528 100644 --- a/src/imgui/window/animation_preview.cpp +++ b/src/imgui/window/animation_preview.cpp @@ -42,51 +42,22 @@ namespace anm2ed::imgui auto& isSound = settings.timelineIsSound; auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; - if (isSound && !anm2.content.sounds.empty()) - if (auto animation = document.animation_get(); animation) - if (animation->triggers.isVisible && !isOnlyShowLayers) - if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER); - trigger.is_visible(anm2::TRIGGER)) - if (anm2.content.sounds.contains(trigger.soundID)) anm2.content.sounds[trigger.soundID].audio.play(); + if (!anm2.content.sounds.empty() && isSound) + { + if (auto animation = document.animation_get(); + animation && animation->triggers.isVisible && (!isOnlyShowLayers || manager.isRecording)) + { + if (auto trigger = animation->triggers.frame_generate(playback.time, anm2::TRIGGER); + trigger.is_visible(anm2::TRIGGER)) + if (anm2.content.sounds.contains(trigger.soundID)) anm2.content.sounds[trigger.soundID].audio.play(mixer); + } + } document.reference.frameTime = playback.time; } if (manager.isRecording) { - if (manager.isRecordingStart) - { - if (settings.renderIsRawAnimation) - { - savedSettings = settings; - settings.previewBackgroundColor = vec4(); - settings.previewIsGrid = false; - settings.previewIsAxes = false; - settings.timelineIsOnlyShowLayers = true; - - savedZoom = zoom; - savedPan = pan; - - if (auto animation = document.animation_get()) - { - auto rect = animation->rect(isRootTransform); - size = vec2(rect.z, rect.w) * scale; - set_to_rect(zoom, pan, rect); - } - - isSizeTrySet = false; - - bind(); - viewport_set(); - clear(settings.previewBackgroundColor); - unbind(); - } - - manager.isRecordingStart = false; - - return; // Need to wait an additional frame. Kind of hacky, but oh well. - } - auto pixels = pixels_get(); renderFrames.push_back(Texture(pixels.data(), size)); @@ -120,7 +91,7 @@ namespace anm2ed::imgui } else { - if (animation_render(ffmpegPath, path, renderFrames, (render::Type)type, size, anm2.info.fps)) + if (animation_render(ffmpegPath, path, renderFrames, audioStream, (render::Type)type, size, anm2.info.fps)) toasts.info(std::format("Exported rendered animation to: {}", path)); else toasts.warning(std::format("Could not output rendered animation: {}", path)); @@ -133,12 +104,49 @@ namespace anm2ed::imgui settings = savedSettings; isSizeTrySet = true; + if (settings.timelineIsSound) audioStream.capture_end(mixer); + playback.isPlaying = false; playback.isFinished = false; manager.isRecording = false; manager.progressPopup.close(); } } + if (manager.isRecordingStart) + { + savedSettings = settings; + + if (settings.timelineIsSound) audioStream.capture_begin(mixer); + + if (settings.renderIsRawAnimation) + { + settings.previewBackgroundColor = vec4(); + settings.previewIsGrid = false; + settings.previewIsAxes = false; + settings.timelineIsOnlyShowLayers = true; + + savedZoom = zoom; + savedPan = pan; + + if (auto animation = document.animation_get()) + { + if (auto rect = animation->rect(isRootTransform); rect != vec4(-1.0f)) + { + size_set(vec2(rect.w, rect.z) * scale); + set_to_rect(zoom, pan, rect); + } + } + + isSizeTrySet = false; + + bind(); + clear(settings.previewBackgroundColor); + unbind(); + } + + manager.isRecordingStart = false; + manager.isRecording = true; + } } void AnimationPreview::update(Manager& manager, Settings& settings, Resources& resources) @@ -270,8 +278,8 @@ namespace anm2ed::imgui auto cursorScreenPos = ImGui::GetCursorScreenPos(); if (isSizeTrySet) size_set(to_vec2(ImGui::GetContentRegionAvail())); - bind(); viewport_set(); + bind(); clear(backgroundColor); if (isAxes) axes_render(shaderAxes, zoom, pan, axesColor); if (isGrid) grid_render(shaderGrid, zoom, pan, gridSize, gridOffset, gridColor); @@ -412,7 +420,7 @@ namespace anm2ed::imgui isPreviewHovered = ImGui::IsItemHovered(); - if (animation && animation->triggers.isVisible && !isOnlyShowLayers) + if (animation && animation->triggers.isVisible && !isOnlyShowLayers && !manager.isRecording) { if (auto trigger = animation->triggers.frame_generate(frameTime, anm2::TRIGGER); trigger.isVisible && trigger.eventID > -1) diff --git a/src/imgui/window/animation_preview.h b/src/imgui/window/animation_preview.h index 33fb991..cbcf380 100644 --- a/src/imgui/window/animation_preview.h +++ b/src/imgui/window/animation_preview.h @@ -1,5 +1,6 @@ #pragma once +#include "audio_stream.h" #include "canvas.h" #include "manager.h" #include "resources.h" @@ -9,6 +10,8 @@ namespace anm2ed::imgui { class AnimationPreview : public Canvas { + MIX_Mixer* mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr); + AudioStream audioStream = AudioStream(mixer); bool isPreviewHovered{}; bool isSizeTrySet{true}; Settings savedSettings{}; diff --git a/src/imgui/window/spritesheets.cpp b/src/imgui/window/spritesheets.cpp index d271f1c..804690a 100644 --- a/src/imgui/window/spritesheets.cpp +++ b/src/imgui/window/spritesheets.cpp @@ -85,7 +85,7 @@ namespace anm2ed::imgui ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); - selection.start(anm2.content.spritesheets.size()); + selection.start(anm2.content.spritesheets.size(), ImGuiMultiSelectFlags_ClearOnEscape); for (auto& [id, spritesheet] : anm2.content.spritesheets) { @@ -168,16 +168,16 @@ namespace anm2ed::imgui context_menu(); } + ImGui::EndChild(); ImGui::PopID(); } - selection.finish(); - ImGui::PopStyleVar(2); context_menu(); + selection.finish(); } ImGui::EndChild(); diff --git a/src/imgui/window/timeline.cpp b/src/imgui/window/timeline.cpp index 68b33c5..0558eba 100644 --- a/src/imgui/window/timeline.cpp +++ b/src/imgui/window/timeline.cpp @@ -12,26 +12,8 @@ using namespace glm; namespace anm2ed::imgui { - constexpr auto ROOT_COLOR = ImVec4(0.140f, 0.310f, 0.560f, 1.000f); - constexpr auto ROOT_COLOR_ACTIVE = ImVec4(0.240f, 0.520f, 0.880f, 1.000f); - constexpr auto ROOT_COLOR_HOVERED = ImVec4(0.320f, 0.640f, 1.000f, 1.000f); - - constexpr auto LAYER_COLOR = ImVec4(0.640f, 0.320f, 0.110f, 1.000f); - constexpr auto LAYER_COLOR_ACTIVE = ImVec4(0.840f, 0.450f, 0.170f, 1.000f); - constexpr auto LAYER_COLOR_HOVERED = ImVec4(0.960f, 0.560f, 0.240f, 1.000f); - - constexpr auto NULL_COLOR = ImVec4(0.140f, 0.430f, 0.200f, 1.000f); - constexpr auto NULL_COLOR_ACTIVE = ImVec4(0.250f, 0.650f, 0.350f, 1.000f); - constexpr auto NULL_COLOR_HOVERED = ImVec4(0.350f, 0.800f, 0.480f, 1.000f); - - constexpr auto TRIGGER_COLOR = ImVec4(0.620f, 0.150f, 0.260f, 1.000f); - constexpr auto TRIGGER_COLOR_ACTIVE = ImVec4(0.820f, 0.250f, 0.380f, 1.000f); - constexpr auto TRIGGER_COLOR_HOVERED = ImVec4(0.950f, 0.330f, 0.490f, 1.000f); - constexpr auto COLOR_HIDDEN_MULTIPLIER = vec4(0.5f, 0.5f, 0.5f, 1.000f); - constexpr auto FRAME_TIMELINE_COLOR = ImVec4(0.106f, 0.184f, 0.278f, 1.000f); - constexpr auto FRAME_BORDER_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.15f); constexpr auto FRAME_MULTIPLE_OVERLAY_COLOR = ImVec4(1.0f, 1.0f, 1.0f, 0.05f); constexpr auto PLAYHEAD_LINE_THICKNESS = 4.0f; @@ -47,1019 +29,6 @@ namespace anm2ed::imgui - Press {} to extend the selected frame, by one frame. - Hold Alt while clicking a non-trigger frame to toggle interpolation.)"; - void Timeline::context_menu(Document& document, Settings& settings, Clipboard& clipboard) - { - auto& hoveredFrame = document.hoveredFrame; - auto& anm2 = document.anm2; - auto& reference = document.reference; - - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); - - auto copy = [&]() - { - if (auto frame = anm2.frame_get(hoveredFrame)) clipboard.set(frame->to_string(hoveredFrame.itemType)); - }; - - auto cut = [&]() - { - copy(); - auto frames_delete = [&]() - { - if (auto item = anm2.item_get(reference); item) - { - item->frames.erase(item->frames.begin() + reference.frameIndex); - reference.frameIndex = glm::max(-1, --reference.frameIndex); - } - }; - - DOCUMENT_EDIT(document, "Cut Frame(s)", Document::FRAMES, frames_delete()); - }; - - auto paste = [&]() - { - if (auto item = document.item_get()) - { - document.snapshot("Paste Frame(s)"); - std::set indices{}; - std::string errorString{}; - auto start = reference.frameIndex + 1; - if (item->frames_deserialize(clipboard.get(), reference.itemType, start, indices, &errorString)) - document.change(Document::FRAMES); - else - toasts.error(std::format("Failed to deserialize frame(s): {}", errorString)); - } - else - toasts.error(std::format("Failed to deserialize frame(s): select an item first!")); - }; - - if (shortcut(settings.shortcutCut, shortcut::FOCUSED)) cut(); - if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); - if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(); - - if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) - { - if (ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, hoveredFrame != anm2::Reference{})) cut(); - if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, hoveredFrame != anm2::Reference{})) copy(); - - if (ImGui::MenuItem("Paste", nullptr, false, !clipboard.is_empty())) paste(); - ImGui::EndPopup(); - } - - ImGui::PopStyleVar(2); - } - - void Timeline::item_child(Manager& manager, Document& document, anm2::Animation* animation, Settings& settings, - Resources& resources, Clipboard& clipboard, anm2::Type type, int id, int& index) - { - auto& anm2 = document.anm2; - auto& reference = document.reference; - - auto item = animation ? animation->item_get(type, id) : nullptr; - auto isVisible = item ? item->isVisible : false; - auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; - if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false; - auto isActive = reference.itemType == type && reference.itemID == id; - std::string label = "##None"; - icon::Type icon{}; - ImVec4 color{}; - - switch (type) - { - case anm2::ROOT: - label = "Root"; - icon = icon::ROOT; - color = isActive ? ROOT_COLOR_ACTIVE : ROOT_COLOR; - break; - case anm2::LAYER: - label = std::format("#{} {} (Spritesheet: #{})", id, anm2.content.layers.at(id).name, - anm2.content.layers[id].spritesheetID); - icon = icon::LAYER; - color = isActive ? LAYER_COLOR_ACTIVE : LAYER_COLOR; - break; - case anm2::NULL_: - label = std::format("#{} {}", id, anm2.content.nulls[id].name); - icon = icon::NULL_; - color = isActive ? NULL_COLOR_ACTIVE : NULL_COLOR; - break; - case anm2::TRIGGER: - label = "Triggers"; - icon = icon::TRIGGERS; - color = isActive ? TRIGGER_COLOR_ACTIVE : TRIGGER_COLOR; - break; - default: - break; - } - - color = !isVisible ? to_imvec4(to_vec4(color) * COLOR_HIDDEN_MULTIPLIER) : color; - ImGui::PushStyleColor(ImGuiCol_ChildBg, color); - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); - - auto itemSize = ImVec2(ImGui::GetContentRegionAvail().x, - ImGui::GetTextLineHeightWithSpacing() + (ImGui::GetStyle().WindowPadding.y * 2)); - - if (ImGui::BeginChild(label.c_str(), itemSize, ImGuiChildFlags_Borders)) - { - if (type != anm2::NONE) - { - anm2::Reference itemReference = {reference.animationIndex, type, id}; - - if (ImGui::IsWindowHovered()) - { - if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) - { - switch (type) - { - case anm2::LAYER: - manager.layer_properties_open(id); // Handled in layers.cpp - break; - case anm2::NULL_: - manager.null_properties_open(id); // Handled in layers.cpp - default: - break; - } - } - - if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) reference = itemReference; - } - - ImGui::Image(resources.icons[icon].id, icon_size_get()); - ImGui::SameLine(); - ImGui::TextUnformatted(label.c_str()); - - anm2::Item* item = animation->item_get(type, id); - bool& isVisible = item->isVisible; - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4()); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); - - ImGui::SetCursorPos(ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, - (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); - int visibleIcon = isVisible ? icon::VISIBLE : icon::INVISIBLE; - - if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, icon_size_get())) - DOCUMENT_EDIT(document, "Item Visibility", Document::FRAMES, isVisible = !isVisible); - ImGui::SetItemTooltip(isVisible ? "The item is shown. Press to hide." : "The item is hidden. Press to show."); - - if (type == anm2::NULL_) - { - auto& null = anm2.content.nulls.at(id); - auto& isShowRect = null.isShowRect; - - auto rectIcon = isShowRect ? icon::SHOW_RECT : icon::HIDE_RECT; - ImGui::SetCursorPos( - ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x, - (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); - if (ImGui::ImageButton("##Rect Toggle", resources.icons[rectIcon].id, icon_size_get())) - DOCUMENT_EDIT(document, "Null Rect", Document::FRAMES, null.isShowRect = !null.isShowRect); - ImGui::SetItemTooltip(isShowRect ? "The null's rect is shown. Press to hide." - : "The null's rect is hidden. Press to show."); - } - - ImGui::PopStyleVar(); - ImGui::PopStyleColor(3); - } - else - { - auto cursorPos = ImGui::GetCursorPos(); - auto& isShowUnused = settings.timelineIsShowUnused; - - ImGui::SetCursorPos(ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, - (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4()); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); - - auto unusedIcon = isShowUnused ? icon::SHOW_UNUSED : icon::HIDE_UNUSED; - if (ImGui::ImageButton("##Unused Toggle", resources.icons[unusedIcon].id, icon_size_get())) - isShowUnused = !isShowUnused; - ImGui::SetItemTooltip(isShowUnused ? "Unused layers/nulls are shown. Press to hide." - : "Unused layers/nulls are hidden. Press to show."); - - auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; - auto layersIcon = isOnlyShowLayers ? icon::SHOW_LAYERS : icon::HIDE_LAYERS; - - ImGui::SetCursorPos( - ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x, - (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); - - if (ImGui::ImageButton("##Layers Toggle", resources.icons[layersIcon].id, icon_size_get())) - isOnlyShowLayers = !isOnlyShowLayers; - ImGui::SetItemTooltip(isOnlyShowLayers ? "Only layers are visible. Press to show all items." - : "All items are visible. Press to only show layers."); - - ImGui::PopStyleVar(); - ImGui::PopStyleColor(3); - - ImGui::SetCursorPos(cursorPos); - - ImGui::BeginDisabled(); - ImGui::Text("(?)"); - ImGui::SetItemTooltip("%s", std::format(HELP_FORMAT, settings.shortcutNextFrame, settings.shortcutPreviousFrame, - settings.shortcutShortenFrame, settings.shortcutExtendFrame) - .c_str()); - ImGui::EndDisabled(); - } - } - ImGui::EndChild(); - ImGui::PopStyleColor(); - ImGui::PopStyleVar(2); - index++; - } - - void Timeline::items_child(Manager& manager, Document& document, anm2::Animation* animation, Settings& settings, - Resources& resources, Clipboard& clipboard) - { - auto& reference = document.reference; - - auto itemsChildSize = ImVec2(ImGui::GetTextLineHeightWithSpacing() * 15, ImGui::GetContentRegionAvail().y); - - if (ImGui::BeginChild("##Items Child", itemsChildSize, ImGuiChildFlags_Borders)) - { - auto itemsListChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - - ImGui::GetTextLineHeightWithSpacing() - - ImGui::GetStyle().ItemSpacing.y * 2); - - if (ImGui::BeginChild("##Items List Child", itemsListChildSize, ImGuiChildFlags_Borders, - ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar)) - { - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2()); - ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 0.0f); - if (ImGui::BeginTable("##Item Table", 1, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY)) - { - ImGui::GetCurrentWindow()->Flags |= ImGuiWindowFlags_NoScrollWithMouse; - ImGui::SetScrollY(scroll.y); - - int index{}; - - ImGui::TableSetupScrollFreeze(0, 1); - ImGui::TableSetupColumn("##Items"); - - auto item_child_row = [&](anm2::Type type, int id = -1) - { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - item_child(manager, document, animation, settings, resources, clipboard, type, id, index); - }; - - item_child_row(anm2::NONE); - - if (animation) - { - item_child_row(anm2::ROOT); - - for (auto& id : animation->layerOrder) - { - if (anm2::Item* item = animation->item_get(anm2::LAYER, id); item) - if (!settings.timelineIsShowUnused && item->frames.empty()) continue; - - item_child_row(anm2::LAYER, id); - } - - for (auto& id : animation->nullAnimations | std::views::keys) - { - if (anm2::Item* item = animation->item_get(anm2::NULL_, id); item) - if (!settings.timelineIsShowUnused && item->frames.empty()) continue; - - item_child_row(anm2::NULL_, id); - } - - item_child_row(anm2::TRIGGER); - } - - if (isHorizontalScroll && ImGui::GetCurrentWindow()->ScrollbarY) - { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Dummy(ImVec2(0, style.ScrollbarSize)); - } - - ImGui::EndTable(); - } - ImGui::PopStyleVar(2); - } - ImGui::EndChild(); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); - - ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + style.WindowPadding.x, ImGui::GetCursorPosY())); - auto widgetSize = widget_size_with_row_get(2, ImGui::GetContentRegionAvail().x - style.WindowPadding.x); - - ImGui::BeginDisabled(!animation); - { - shortcut(settings.shortcutAdd); - if (ImGui::Button("Add", widgetSize)) propertiesPopup.open(); - set_item_tooltip_shortcut("Add a new item to the animation.", settings.shortcutAdd); - ImGui::SameLine(); - - ImGui::BeginDisabled(!document.item_get() && reference.itemType != anm2::LAYER && - reference.itemType != anm2::NULL_); - { - shortcut(settings.shortcutRemove); - if (ImGui::Button("Remove", widgetSize)) - { - auto remove = [&]() - { - animation->item_remove(reference.itemType, reference.itemID); - reference = {reference.animationIndex}; - }; - - DOCUMENT_EDIT(document, "Remove Item", Document::ITEMS, remove()); - } - set_item_tooltip_shortcut("Remove the selected items from the animation.", settings.shortcutRemove); - } - ImGui::EndDisabled(); - } - ImGui::EndDisabled(); - - ImGui::PopStyleVar(); - } - ImGui::EndChild(); - } - - void Timeline::frame_child(Document& document, anm2::Animation* animation, Settings& settings, Resources& resources, - Clipboard& clipboard, anm2::Type type, int id, int& index, float width) - { - auto& anm2 = document.anm2; - auto& playback = document.playback; - auto& reference = document.reference; - auto& hoveredFrame = document.hoveredFrame; - auto item = animation ? animation->item_get(type, id) : nullptr; - auto isVisible = item ? item->isVisible : false; - auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; - if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false; - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); - - auto childSize = ImVec2(width, ImGui::GetTextLineHeightWithSpacing() + (ImGui::GetStyle().WindowPadding.y * 2)); - ImVec4 color{}; - ImVec4 colorActive{}; - ImVec4 colorHovered{}; - ImVec4 colorHidden{}; - ImVec4 colorActiveHidden{}; - ImVec4 colorHoveredHidden{}; - - ImGui::PopStyleVar(2); - - ImGui::PushID(index); - - switch (type) - { - case anm2::ROOT: - color = ROOT_COLOR; - colorActive = ROOT_COLOR_ACTIVE; - colorHovered = ROOT_COLOR_HOVERED; - break; - case anm2::LAYER: - color = LAYER_COLOR; - colorActive = LAYER_COLOR_ACTIVE; - colorHovered = LAYER_COLOR_HOVERED; - break; - case anm2::NULL_: - color = NULL_COLOR; - colorActive = NULL_COLOR_ACTIVE; - colorHovered = NULL_COLOR_HOVERED; - break; - case anm2::TRIGGER: - color = TRIGGER_COLOR; - colorActive = TRIGGER_COLOR_ACTIVE; - colorHovered = TRIGGER_COLOR_HOVERED; - break; - default: - color = ImGui::GetStyleColorVec4(ImGuiCol_Button); - colorActive = ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive); - colorHovered = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); - break; - } - - colorHidden = to_imvec4(to_vec4(color) * COLOR_HIDDEN_MULTIPLIER); - colorActiveHidden = to_imvec4(to_vec4(colorActive) * COLOR_HIDDEN_MULTIPLIER); - colorHoveredHidden = to_imvec4(to_vec4(colorHovered) * COLOR_HIDDEN_MULTIPLIER); - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); - - if (ImGui::BeginChild("##Frames Child", childSize, ImGuiChildFlags_Borders)) - { - auto length = animation ? animation->frameNum : anm2.animations.length(); - auto frameSize = ImVec2(ImGui::GetTextLineHeight(), ImGui::GetContentRegionAvail().y); - auto framesSize = ImVec2(frameSize.x * length, frameSize.y); - auto cursorPos = ImGui::GetCursorPos(); - auto cursorScreenPos = ImGui::GetCursorScreenPos(); - auto imageSize = vec2(ImGui::GetTextLineHeight()); - auto border = ImGui::GetStyle().FrameBorderSize; - auto borderLineLength = frameSize.y / 5; - auto scrollX = ImGui::GetScrollX(); - auto available = ImGui::GetContentRegionAvail(); - auto frameMin = std::max(0, (int)std::floor(scrollX / frameSize.x) - 1); - auto frameMax = std::min(anm2::FRAME_NUM_MAX, (int)std::ceil(scrollX + available.x / frameSize.x) + 1); - auto drawList = ImGui::GetWindowDrawList(); - pickerLineDrawList = drawList; - - if (type == anm2::NONE) - { - drawList->AddRectFilled(cursorScreenPos, - ImVec2(cursorScreenPos.x + framesSize.x, cursorScreenPos.y + framesSize.y), - ImGui::GetColorU32(FRAME_TIMELINE_COLOR)); - - for (int i = frameMin; i < frameMax; i++) - { - auto frameScreenPos = ImVec2(cursorScreenPos.x + frameSize.x * (float)i, cursorScreenPos.y); - - drawList->AddRect(frameScreenPos, ImVec2(frameScreenPos.x + border, frameScreenPos.y + borderLineLength), - ImGui::GetColorU32(FRAME_BORDER_COLOR)); - - drawList->AddRect(ImVec2(frameScreenPos.x, frameScreenPos.y + frameSize.y - borderLineLength), - ImVec2(frameScreenPos.x + border, frameScreenPos.y + frameSize.y), - ImGui::GetColorU32(FRAME_BORDER_COLOR)); - - if (i % FRAME_MULTIPLE == 0) - { - auto string = std::to_string(i); - auto textSize = ImGui::CalcTextSize(string.c_str()); - auto textPos = ImVec2(frameScreenPos.x + (frameSize.x - textSize.x) / 2, - frameScreenPos.y + (frameSize.y - textSize.y) / 2); - - drawList->AddRectFilled(frameScreenPos, - ImVec2(frameScreenPos.x + frameSize.x, frameScreenPos.y + frameSize.y), - ImGui::GetColorU32(FRAME_MULTIPLE_OVERLAY_COLOR)); - - drawList->AddText(textPos, ImGui::GetColorU32(TEXT_MULTIPLE_COLOR), string.c_str()); - } - } - - if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && ImGui::IsMouseDown(0)) - isDragging = true; - - if (isDragging) - { - auto childPos = ImGui::GetWindowPos(); - auto mousePos = ImGui::GetIO().MousePos; - auto localMousePos = ImVec2(mousePos.x - childPos.x, mousePos.y - childPos.y); - playback.time = floorf(localMousePos.x / frameSize.x); - reference.frameTime = playback.time; - } - - playback.clamp(settings.playbackIsClampPlayhead ? length : anm2::FRAME_NUM_MAX); - - if (ImGui::IsMouseReleased(0)) isDragging = false; - - ImGui::SetCursorPos(ImVec2(cursorPos.x + frameSize.x * floorf(playback.time), cursorPos.y)); - ImGui::Image(resources.icons[icon::PLAYHEAD].id, frameSize); - } - else if (animation) - { - anm2::Reference itemReference = {reference.animationIndex, type, id}; - if (ImGui::IsWindowHovered() && ImGui::IsMouseReleased(0)) reference = itemReference; - - for (int i = frameMin; i < frameMax; i++) - { - auto frameScreenPos = ImVec2(cursorScreenPos.x + (frameSize.x * i), cursorScreenPos.y); - - if (i % FRAME_MULTIPLE == 0) - { - drawList->AddRectFilled(frameScreenPos, - ImVec2(frameScreenPos.x + frameSize.x, frameScreenPos.y + frameSize.y), - ImGui::GetColorU32(FRAME_MULTIPLE_OVERLAY_COLOR)); - } - drawList->AddRect(frameScreenPos, ImVec2(frameScreenPos.x + frameSize.x, frameScreenPos.y + frameSize.y), - ImGui::GetColorU32(FRAME_BORDER_COLOR)); - } - - auto item = animation->item_get(type, id); - - auto frameTime = 0; - anm2::Reference baseReference = {reference.animationIndex, reference.itemType, reference.itemID, - reference.frameIndex}; - - for (auto [i, frame] : std::views::enumerate(item->frames)) - { - anm2::Reference frameReference = {reference.animationIndex, type, id, (int)i}; - auto isSelected = baseReference == frameReference; - auto isFrameVisible = isVisible && frame.isVisible; - - ImGui::PushID(i); - auto size = ImVec2(frameSize.x * frame.delay, frameSize.y); - - auto icon = type == anm2::TRIGGER ? icon::TRIGGER - : frame.isInterpolated ? icon::INTERPOLATED - : icon::UNINTERPOLATED; - - if (type == anm2::TRIGGER) - ImGui::SetCursorPos(ImVec2(cursorPos.x + frameSize.x * frame.atFrame, cursorPos.y)); - - ImGui::PushStyleColor(ImGuiCol_Button, isFrameVisible ? color : colorHidden); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, isFrameVisible ? colorActive : colorActiveHidden); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, isFrameVisible ? colorHovered : colorHoveredHidden); - - if (isSelected) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); - - if (ImGui::Button("##Frame Button", size)) - { - if (type != anm2::TRIGGER && ImGui::IsKeyDown(ImGuiMod_Alt)) - DOCUMENT_EDIT(document, "Frame Interpolation", Document::FRAMES, - frame.isInterpolated = !frame.isInterpolated); - if (type == anm2::LAYER) - { - document.spritesheet.reference = anm2.content.layers[id].spritesheetID; - document.layer.selection = {id}; - } - reference = frameReference; - reference.frameTime = frameTime; - } - if (ImGui::IsItemHovered()) hoveredFrame = frameReference; - - if (type != anm2::TRIGGER) ImGui::SameLine(); - - ImGui::PopStyleColor(3); - if (isSelected) ImGui::PopStyleColor(); - - auto imageMin = ImVec2(ImGui::GetItemRectMin().x, - ImGui::GetItemRectMax().y - (ImGui::GetItemRectSize().y / 2) - (imageSize.y / 2)); - auto imageMax = to_imvec2(to_vec2(imageMin) + imageSize); - - drawList->AddImage(resources.icons[icon].id, imageMin, imageMax); - - frameTime += frame.delay; - - ImGui::PopID(); - } - - context_menu(document, settings, clipboard); - } - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - - index++; - ImGui::PopID(); - } - - void Timeline::frames_child(Document& document, anm2::Animation* animation, Settings& settings, Resources& resources, - Clipboard& clipboard) - { - auto& anm2 = document.anm2; - auto& reference = document.reference; - auto& playback = document.playback; - - auto itemsChildWidth = ImGui::GetTextLineHeightWithSpacing() * 15; - - auto cursorPos = ImGui::GetCursorPos(); - ImGui::SetCursorPos(ImVec2(cursorPos.x + itemsChildWidth, cursorPos.y)); - - auto framesChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y); - - if (ImGui::BeginChild("##Frames Child", framesChildSize, ImGuiChildFlags_Borders)) - { - auto viewListChildSize = - ImVec2(ImGui::GetContentRegionAvail().x, - ImGui::GetContentRegionAvail().y - ImGui::GetTextLineHeightWithSpacing() - style.ItemSpacing.y * 2); - - auto childWidth = ImGui::GetContentRegionAvail().x > anm2.animations.length() * ImGui::GetTextLineHeight() - ? ImGui::GetContentRegionAvail().x - : anm2.animations.length() * ImGui::GetTextLineHeight(); - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); - if (ImGui::BeginChild("##Frames List Child", viewListChildSize, true, ImGuiWindowFlags_HorizontalScrollbar)) - { - auto cursorScreenPos = ImGui::GetCursorScreenPos(); - - ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2()); - if (ImGui::BeginTable("##Frames List Table", 1, - ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) - { - ImGuiWindow* window = ImGui::GetCurrentWindow(); - window->Flags |= ImGuiWindowFlags_NoScrollWithMouse; - - scroll.x = ImGui::GetScrollX(); - scroll.y = ImGui::GetScrollY(); - - isHorizontalScroll = window->ScrollbarX; - - if (isWindowHovered) - { - auto& io = ImGui::GetIO(); - auto lineHeight = ImGui::GetTextLineHeightWithSpacing() * 2; - - scroll.x -= io.MouseWheelH * lineHeight; - scroll.y -= io.MouseWheel * lineHeight; - } - - ImGui::SetScrollX(scroll.x); - ImGui::SetScrollY(scroll.y); - - int index{}; - - ImGui::TableSetupScrollFreeze(0, 1); - ImGui::TableSetupColumn("##Frames"); - - auto frames_child_row = [&](anm2::Type type, int id = -1) - { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - frame_child(document, animation, settings, resources, clipboard, type, id, index, childWidth); - }; - - frames_child_row(anm2::NONE); - - //hoveredFrame = anm2::REFERENCE_DEFAULT; - - if (animation) - { - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); - - frames_child_row(anm2::ROOT); - - for (auto& id : animation->layerOrder) - { - if (auto item = animation->item_get(anm2::LAYER, id); item) - if (!settings.timelineIsShowUnused && item->frames.empty()) continue; - - frames_child_row(anm2::LAYER, id); - } - - for (auto& id : animation->nullAnimations | std::views::keys) - { - if (auto item = animation->item_get(anm2::NULL_, id); item) - if (!settings.timelineIsShowUnused && item->frames.empty()) continue; - frames_child_row(anm2::NULL_, id); - } - - frames_child_row(anm2::TRIGGER); - - ImGui::PopStyleVar(); - } - ImGui::EndTable(); - } - - ImDrawList* windowDrawList = ImGui::GetWindowDrawList(); - - auto frameSize = ImVec2(ImGui::GetTextLineHeight(), - ImGui::GetTextLineHeightWithSpacing() + (ImGui::GetStyle().WindowPadding.y * 2)); - auto linePos = ImVec2(cursorScreenPos.x + frameSize.x * floorf(playback.time) + (frameSize.x / 2) - scroll.x, - cursorScreenPos.y + frameSize.y); - auto lineSize = - ImVec2((PLAYHEAD_LINE_THICKNESS / 2.0f), - viewListChildSize.y - frameSize.y - (isHorizontalScroll ? ImGui::GetStyle().ScrollbarSize : 0.0f)); - - auto rectMin = windowDrawList->GetClipRectMin(); - auto rectMax = windowDrawList->GetClipRectMax(); - pickerLineDrawList->PushClipRect(rectMin, rectMax); - pickerLineDrawList->AddRectFilled(linePos, ImVec2(linePos.x + lineSize.x, linePos.y + lineSize.y), - ImGui::GetColorU32(PLAYHEAD_LINE_COLOR)); - pickerLineDrawList->PopClipRect(); - - ImGui::PopStyleVar(); - - context_menu(document, settings, clipboard); - } - ImGui::EndChild(); - ImGui::PopStyleVar(); - - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); - - ImGui::SetCursorPos( - ImVec2(ImGui::GetStyle().WindowPadding.x, ImGui::GetCursorPos().y + ImGui::GetStyle().ItemSpacing.y)); - - auto widgetSize = widget_size_with_row_get(10); - - ImGui::BeginDisabled(!animation); - { - auto label = playback.isPlaying ? "Pause" : "Play"; - auto tooltip = playback.isPlaying ? "Pause the animation." : "Play the animation."; - - shortcut(settings.shortcutPlayPause); - if (ImGui::Button(label, widgetSize)) playback.toggle(); - set_item_tooltip_shortcut(tooltip, settings.shortcutPlayPause); - - ImGui::SameLine(); - - auto item = document.item_get(); - - ImGui::BeginDisabled(!item); - { - shortcut(settings.shortcutAdd); - if (ImGui::Button("Insert Frame", widgetSize)) - { - auto insert_frame = [&]() - { - auto frame = document.frame_get(); - if (frame) - { - item->frames.insert(item->frames.begin() + reference.frameIndex, *frame); - reference.frameIndex++; - } - else if (!item->frames.empty()) - { - auto frame = item->frames.back(); - item->frames.emplace_back(frame); - reference.frameIndex = item->frames.size() - 1; - } - }; - - DOCUMENT_EDIT(document, "Insert Frame", Document::FRAMES, insert_frame()); - } - set_item_tooltip_shortcut("Insert a frame, based on the current selection.", settings.shortcutAdd); - - ImGui::SameLine(); - - ImGui::BeginDisabled(!document.frame_get()); - { - shortcut(settings.shortcutRemove); - if (ImGui::Button("Delete Frame", widgetSize)) - { - auto delete_frame = [&]() - { - item->frames.erase(item->frames.begin() + reference.frameIndex); - reference.frameIndex = glm::max(-1, --reference.frameIndex); - }; - - DOCUMENT_EDIT(document, "Delete Frame(s)", Document::FRAMES, delete_frame()); - } - set_item_tooltip_shortcut("Delete the selected frames.", settings.shortcutRemove); - - ImGui::SameLine(); - - if (ImGui::Button("Bake", widgetSize)) bakePopup.open(); - ImGui::SetItemTooltip("Turn interpolated frames into uninterpolated ones."); - } - ImGui::EndDisabled(); - } - ImGui::EndDisabled(); - - ImGui::SameLine(); - - if (ImGui::Button("Fit Animation Length", widgetSize)) animation->frameNum = animation->length(); - ImGui::SetItemTooltip("The animation length will be set to the effective length of the animation."); - - ImGui::SameLine(); - - ImGui::SetNextItemWidth(widgetSize.x); - ImGui::InputInt("Animation Length", animation ? &animation->frameNum : &dummy_value(), STEP, STEP_FAST, - !animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0); - if (animation) animation->frameNum = clamp(animation->frameNum, anm2::FRAME_NUM_MIN, anm2::FRAME_NUM_MAX); - ImGui::SetItemTooltip("Set the animation's length."); - - ImGui::SameLine(); - - ImGui::SetNextItemWidth(widgetSize.x); - ImGui::Checkbox("Loop", animation ? &animation->isLoop : &dummy_value()); - ImGui::SetItemTooltip("Toggle the animation looping."); - } - ImGui::EndDisabled(); - - ImGui::SameLine(); - - ImGui::SetNextItemWidth(widgetSize.x); - ImGui::InputInt("FPS", &anm2.info.fps, 1, 5); - anm2.info.fps = clamp(anm2.info.fps, anm2::FPS_MIN, anm2::FPS_MAX); - ImGui::SetItemTooltip("Set the FPS of all animations."); - - ImGui::SameLine(); - - ImGui::SetNextItemWidth(widgetSize.x); - input_text_string("Author", &anm2.info.createdBy); - ImGui::SetItemTooltip("Set the author of the document."); - - ImGui::SameLine(); - - ImGui::SetNextItemWidth(widgetSize.x); - ImGui::Checkbox("Sound", &settings.timelineIsSound); - ImGui::SetItemTooltip("Toggle sounds playing with triggers.\nBind sounds to events in the Events window."); - - ImGui::PopStyleVar(); - } - ImGui::EndChild(); - - ImGui::SetCursorPos(cursorPos); - } - - void Timeline::popups(Document& document, anm2::Animation* animation, Settings& settings) - { - auto item_properties_reset = [&]() - { - addItemName.clear(); - addItemSpritesheetID = {}; - addItemID = -1; - isUnusedItemsSet = false; - }; - - auto& anm2 = document.anm2; - auto& reference = document.reference; - - propertiesPopup.trigger(); - - if (ImGui::BeginPopupModal(propertiesPopup.label, &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) - { - auto item_properties_close = [&]() - { - item_properties_reset(); - propertiesPopup.close(); - }; - - auto& type = settings.timelineAddItemType; - auto& locale = settings.timelineAddItemLocale; - auto& source = settings.timelineAddItemSource; - - if (!isUnusedItemsSet) - { - unusedItems = type == anm2::LAYER ? anm2.layers_unused(reference) - : type == anm2::NULL_ ? anm2.nulls_unused(reference) - : std::set{}; - - isUnusedItemsSet = true; - } - - auto footerSize = footer_size_get(); - auto optionsSize = child_size_get(11); - auto itemsSize = ImVec2(0, ImGui::GetContentRegionAvail().y - - (optionsSize.y + footerSize.y + ImGui::GetStyle().ItemSpacing.y * 4)); - if (ImGui::BeginChild("Options", optionsSize, ImGuiChildFlags_Borders)) - { - ImGui::SeparatorText("Type"); - - auto size = ImVec2(ImGui::GetContentRegionAvail().x * 0.5f, ImGui::GetFrameHeightWithSpacing()); - - if (ImGui::BeginChild("Type Layer", size)) - { - ImGui::RadioButton("Layer", &type, anm2::LAYER); - ImGui::SetItemTooltip("Layers are a basic visual element in an animation, used for displaying spritesheets."); - } - ImGui::EndChild(); - - ImGui::SameLine(); - - if (ImGui::BeginChild("Type Null", size)) - { - ImGui::RadioButton("Null", &type, anm2::NULL_); - ImGui::SetItemTooltip( - "Nulls are invisible elements in an animation, used for interfacing with a game engine."); - } - ImGui::EndChild(); - - ImGui::SeparatorText("Source"); - - bool isNewOnly = unusedItems.empty(); - if (isNewOnly) source = source::NEW; - - if (ImGui::BeginChild("Source New", size)) - { - ImGui::RadioButton("New", &source, source::NEW); - ImGui::SetItemTooltip("Create a new item to be used."); - } - ImGui::EndChild(); - - ImGui::SameLine(); - - if (ImGui::BeginChild("Source Existing", size)) - { - ImGui::BeginDisabled(isNewOnly); - ImGui::RadioButton("Existing", &source, source::EXISTING); - ImGui::EndDisabled(); - ImGui::SetItemTooltip("Use a pre-existing, presently unused item."); - } - ImGui::EndChild(); - - ImGui::SeparatorText("Locale"); - - if (ImGui::BeginChild("Locale Global", size)) - { - ImGui::RadioButton("Global", &locale, locale::GLOBAL); - ImGui::SetItemTooltip("The item will be inserted into all animations, if not already present."); - } - ImGui::EndChild(); - - ImGui::SameLine(); - - if (ImGui::BeginChild("Locale Local", size)) - { - ImGui::RadioButton("Local", &locale, locale::LOCAL); - ImGui::SetItemTooltip("The item will only be inserted into this animation."); - } - ImGui::EndChild(); - - ImGui::SeparatorText("Options"); - - ImGui::BeginDisabled(source == source::EXISTING); - { - input_text_string("Name", &addItemName); - ImGui::SetItemTooltip("Set the item's name."); - ImGui::BeginDisabled(type != anm2::LAYER); - { - combo_negative_one_indexed("Spritesheet", &addItemSpritesheetID, document.spritesheet.labels); - ImGui::SetItemTooltip("Set the layer item's spritesheet."); - } - ImGui::EndDisabled(); - } - ImGui::EndDisabled(); - } - ImGui::EndChild(); - - if (ImGui::BeginChild("Items", itemsSize, ImGuiChildFlags_Borders)) - { - if (animation && source == source::EXISTING) - { - for (auto id : unusedItems) - { - auto isSelected = addItemID == id; - - ImGui::PushID(id); - - if (type == anm2::LAYER) - { - auto& layer = anm2.content.layers[id]; - if (ImGui::Selectable( - std::format("#{} {} (Spritesheet: #{})", id, layer.name, layer.spritesheetID).c_str(), - isSelected)) - addItemID = id; - } - else if (type == anm2::NULL_) - { - auto& null = anm2.content.nulls[id]; - if (ImGui::Selectable(std::format("#{} {}", id, null.name).c_str(), isSelected)) addItemID = id; - } - - ImGui::PopID(); - } - } - } - ImGui::EndChild(); - - auto widgetSize = widget_size_with_row_get(2); - - if (ImGui::Button("Add", widgetSize)) - { - anm2::Reference addReference{}; - - document.snapshot("Add Item"); - if (type == anm2::LAYER) - addReference = anm2.layer_animation_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName, - addItemSpritesheetID - 1, (locale::Type)locale); - else if (type == anm2::NULL_) - addReference = anm2.null_animation_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName, - (locale::Type)locale); - - document.change(Document::ITEMS); - - reference = addReference; - - item_properties_close(); - } - ImGui::SetItemTooltip("Add the item, with the settings specified."); - - ImGui::SameLine(); - - if (ImGui::Button("Cancel", widgetSize)) item_properties_close(); - ImGui::SetItemTooltip("Cancel adding an item."); - - ImGui::EndPopup(); - } - - bakePopup.trigger(); - - if (ImGui::BeginPopupModal(bakePopup.label, &bakePopup.isOpen, ImGuiWindowFlags_NoResize)) - { - auto& interval = settings.bakeInterval; - auto& isRoundRotation = settings.bakeIsRoundRotation; - auto& isRoundScale = settings.bakeIsRoundScale; - - auto frame = document.frame_get(); - - input_int_range("Interval", interval, anm2::FRAME_DELAY_MIN, frame ? frame->delay : anm2::FRAME_DELAY_MIN); - ImGui::SetItemTooltip("Set the maximum delay of each frame that will be baked."); - - ImGui::Checkbox("Round Rotation", &isRoundRotation); - ImGui::SetItemTooltip("Rotation will be rounded to the nearest whole number."); - - ImGui::Checkbox("Round Scale", &isRoundScale); - ImGui::SetItemTooltip("Scale will be rounded to the nearest whole number."); - - auto widgetSize = widget_size_with_row_get(2); - - if (ImGui::Button("Bake", widgetSize)) - { - if (auto item = document.item_get()) - DOCUMENT_EDIT(document, "Bake Frames", Document::FRAMES, - item->frames_bake(reference.frameIndex, interval, isRoundScale, isRoundRotation)); - bakePopup.close(); - } - ImGui::SetItemTooltip("Bake the selected frame(s) with the options selected."); - - ImGui::SameLine(); - - if (ImGui::Button("Cancel", widgetSize)) bakePopup.close(); - ImGui::SetItemTooltip("Cancel baking frames."); - - ImGui::EndPopup(); - } - } - void Timeline::update(Manager& manager, Settings& settings, Resources& resources, Clipboard& clipboard) { auto& document = *manager.get(); @@ -1069,17 +38,938 @@ namespace anm2ed::imgui style = ImGui::GetStyle(); + auto context_menu = [&]() + { + auto& hoveredFrame = document.hoveredFrame; + auto& anm2 = document.anm2; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); + + auto copy = [&]() + { + if (auto frame = anm2.frame_get(hoveredFrame)) clipboard.set(frame->to_string(hoveredFrame.itemType)); + }; + + auto cut = [&]() + { + copy(); + auto frames_delete = [&]() + { + if (auto item = anm2.item_get(reference); item) + { + item->frames.erase(item->frames.begin() + reference.frameIndex); + reference.frameIndex = glm::max(-1, --reference.frameIndex); + } + }; + + DOCUMENT_EDIT(document, "Cut Frame(s)", Document::FRAMES, frames_delete()); + }; + + auto paste = [&]() + { + if (auto item = document.item_get()) + { + document.snapshot("Paste Frame(s)"); + std::set indices{}; + std::string errorString{}; + auto start = reference.frameIndex + 1; + if (item->frames_deserialize(clipboard.get(), reference.itemType, start, indices, &errorString)) + document.change(Document::FRAMES); + else + toasts.error(std::format("Failed to deserialize frame(s): {}", errorString)); + } + else + toasts.error(std::format("Failed to deserialize frame(s): select an item first!")); + }; + + if (shortcut(settings.shortcutCut, shortcut::FOCUSED)) cut(); + if (shortcut(settings.shortcutCopy, shortcut::FOCUSED)) copy(); + if (shortcut(settings.shortcutPaste, shortcut::FOCUSED)) paste(); + + if (ImGui::BeginPopupContextWindow("##Context Menu", ImGuiPopupFlags_MouseButtonRight)) + { + if (ImGui::MenuItem("Cut", settings.shortcutCut.c_str(), false, hoveredFrame != anm2::Reference{})) cut(); + if (ImGui::MenuItem("Copy", settings.shortcutCopy.c_str(), false, hoveredFrame != anm2::Reference{})) copy(); + if (ImGui::MenuItem("Paste", nullptr, false, !clipboard.is_empty())) paste(); + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(2); + }; + + auto item_child = [&](anm2::Type type, int id, int& index) + { + auto& anm2 = document.anm2; + + auto item = animation ? animation->item_get(type, id) : nullptr; + auto isVisible = item ? item->isVisible : false; + auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; + if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false; + auto isActive = reference.itemType == type && reference.itemID == id; + + auto label = type == anm2::LAYER ? std::format(anm2::LAYER_FORMAT, id, anm2.content.layers.at(id).name, + anm2.content.layers[id].spritesheetID) + : type == anm2::NULL_ ? std::format(anm2::NULL_FORMAT, id, anm2.content.nulls[id].name) + : anm2::TYPE_STRINGS[type]; + auto icon = anm2::TYPE_ICONS[type]; + auto color = to_imvec4(isActive ? anm2::TYPE_COLOR_ACTIVE[type] : anm2::TYPE_COLOR[type]); + color = !isVisible ? to_imvec4(to_vec4(color) * COLOR_HIDDEN_MULTIPLIER) : color; + ImGui::PushStyleColor(ImGuiCol_ChildBg, color); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); + + auto itemSize = ImVec2(ImGui::GetContentRegionAvail().x, + ImGui::GetTextLineHeightWithSpacing() + (ImGui::GetStyle().WindowPadding.y * 2)); + + if (ImGui::BeginChild(label.c_str(), itemSize, ImGuiChildFlags_Borders)) + { + if (type != anm2::NONE) + { + anm2::Reference itemReference = {reference.animationIndex, type, id}; + + if (ImGui::IsWindowHovered()) + { + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + { + switch (type) + { + case anm2::LAYER: + manager.layer_properties_open(id); + break; + case anm2::NULL_: + manager.null_properties_open(id); + default: + break; + } + } + + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) reference = itemReference; + } + + ImGui::Image(resources.icons[icon].id, icon_size_get()); + ImGui::SameLine(); + ImGui::TextUnformatted(label.c_str()); + + anm2::Item* itemPtr = animation->item_get(type, id); + bool& itemVisible = itemPtr->isVisible; + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4()); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); + + ImGui::SetCursorPos( + ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, + (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); + int visibleIcon = itemVisible ? icon::VISIBLE : icon::INVISIBLE; + + if (ImGui::ImageButton("##Visible Toggle", resources.icons[visibleIcon].id, icon_size_get())) + DOCUMENT_EDIT(document, "Item Visibility", Document::FRAMES, itemVisible = !itemVisible); + ImGui::SetItemTooltip(itemVisible ? "The item is shown. Press to hide." + : "The item is hidden. Press to show."); + + if (type == anm2::NULL_) + { + auto& null = anm2.content.nulls.at(id); + auto& isShowRect = null.isShowRect; + + auto rectIcon = isShowRect ? icon::SHOW_RECT : icon::HIDE_RECT; + ImGui::SetCursorPos( + ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x, + (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); + if (ImGui::ImageButton("##Rect Toggle", resources.icons[rectIcon].id, icon_size_get())) + DOCUMENT_EDIT(document, "Null Rect", Document::FRAMES, null.isShowRect = !null.isShowRect); + ImGui::SetItemTooltip(isShowRect ? "The null's rect is shown. Press to hide." + : "The null's rect is hidden. Press to show."); + } + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(3); + } + else + { + auto cursorPos = ImGui::GetCursorPos(); + auto& isShowUnused = settings.timelineIsShowUnused; + + ImGui::SetCursorPos( + ImVec2(itemSize.x - ImGui::GetTextLineHeightWithSpacing() - ImGui::GetStyle().ItemSpacing.x, + (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4()); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4()); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4()); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); + + auto unusedIcon = isShowUnused ? icon::SHOW_UNUSED : icon::HIDE_UNUSED; + if (ImGui::ImageButton("##Unused Toggle", resources.icons[unusedIcon].id, icon_size_get())) + isShowUnused = !isShowUnused; + ImGui::SetItemTooltip(isShowUnused ? "Unused layers/nulls are shown. Press to hide." + : "Unused layers/nulls are hidden. Press to show."); + + auto& showLayersOnly = settings.timelineIsOnlyShowLayers; + auto layersIcon = showLayersOnly ? icon::SHOW_LAYERS : icon::HIDE_LAYERS; + + ImGui::SetCursorPos( + ImVec2(itemSize.x - (ImGui::GetTextLineHeightWithSpacing() * 2) - ImGui::GetStyle().ItemSpacing.x, + (itemSize.y - ImGui::GetTextLineHeightWithSpacing()) / 2)); + + if (ImGui::ImageButton("##Layers Toggle", resources.icons[layersIcon].id, icon_size_get())) + showLayersOnly = !showLayersOnly; + ImGui::SetItemTooltip(showLayersOnly ? "Only layers are visible. Press to show all items." + : "All items are visible. Press to only show layers."); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(3); + + ImGui::SetCursorPos(cursorPos); + + ImGui::BeginDisabled(); + ImGui::Text("(?)"); + ImGui::SetItemTooltip("%s", + std::format(HELP_FORMAT, settings.shortcutNextFrame, settings.shortcutPreviousFrame, + settings.shortcutShortenFrame, settings.shortcutExtendFrame) + .c_str()); + ImGui::EndDisabled(); + } + } + ImGui::EndChild(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(2); + index++; + }; + + auto items_child = [&]() + { + auto itemsChildSize = ImVec2(ImGui::GetTextLineHeightWithSpacing() * 15, ImGui::GetContentRegionAvail().y); + + if (ImGui::BeginChild("##Items Child", itemsChildSize, ImGuiChildFlags_Borders)) + { + auto itemsListChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - + ImGui::GetTextLineHeightWithSpacing() - + ImGui::GetStyle().ItemSpacing.y * 2); + + if (ImGui::BeginChild("##Items List Child", itemsListChildSize, ImGuiChildFlags_Borders, + ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar)) + { + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2()); + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 0.0f); + if (ImGui::BeginTable("##Item Table", 1, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY)) + { + ImGui::GetCurrentWindow()->Flags |= ImGuiWindowFlags_NoScrollWithMouse; + ImGui::SetScrollY(scroll.y); + + int index{}; + + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("##Items"); + + auto item_child_row = [&](anm2::Type type, int id = -1) + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + item_child(type, id, index); + }; + + item_child_row(anm2::NONE); + + if (animation) + { + item_child_row(anm2::ROOT); + + for (auto& id : animation->layerOrder) + { + if (!settings.timelineIsShowUnused && animation->layerAnimations[id].frames.empty()) continue; + item_child_row(anm2::LAYER, id); + } + + for (auto& [id, nullAnimation] : animation->nullAnimations) + { + if (!settings.timelineIsShowUnused && nullAnimation.frames.empty()) continue; + item_child_row(anm2::NULL_, id); + } + + item_child_row(anm2::TRIGGER); + } + + if (isHorizontalScroll && ImGui::GetCurrentWindow()->ScrollbarY) + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Dummy(ImVec2(0, style.ScrollbarSize)); + } + + ImGui::EndTable(); + } + ImGui::PopStyleVar(2); + } + ImGui::EndChild(); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); + + ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + style.WindowPadding.x, ImGui::GetCursorPosY())); + auto widgetSize = widget_size_with_row_get(2, ImGui::GetContentRegionAvail().x - style.WindowPadding.x); + + ImGui::BeginDisabled(!animation); + { + shortcut(settings.shortcutAdd); + if (ImGui::Button("Add", widgetSize)) propertiesPopup.open(); + set_item_tooltip_shortcut("Add a new item to the animation.", settings.shortcutAdd); + ImGui::SameLine(); + + ImGui::BeginDisabled(!document.item_get() && reference.itemType != anm2::LAYER && + reference.itemType != anm2::NULL_); + { + shortcut(settings.shortcutRemove); + if (ImGui::Button("Remove", widgetSize)) + { + auto remove = [&]() + { + animation->item_remove(reference.itemType, reference.itemID); + reference = {reference.animationIndex}; + }; + + DOCUMENT_EDIT(document, "Remove Item", Document::ITEMS, remove()); + } + set_item_tooltip_shortcut("Remove the selected item(s) from the animation.", settings.shortcutRemove); + } + ImGui::EndDisabled(); + } + ImGui::EndDisabled(); + + ImGui::PopStyleVar(); + } + ImGui::EndChild(); + }; + + auto frame_child = [&](anm2::Type type, int id, int& index, float width) + { + auto& anm2 = document.anm2; + auto& hoveredFrame = document.hoveredFrame; + + auto item = animation ? animation->item_get(type, id) : nullptr; + auto isVisible = item ? item->isVisible : false; + auto& isOnlyShowLayers = settings.timelineIsOnlyShowLayers; + if (isOnlyShowLayers && type != anm2::LAYER) isVisible = false; + + auto color = to_imvec4(anm2::TYPE_COLOR[type]); + auto colorActive = to_imvec4(anm2::TYPE_COLOR_ACTIVE[type]); + auto colorHovered = to_imvec4(anm2::TYPE_COLOR_HOVERED[type]); + auto colorHidden = to_imvec4(to_vec4(color) * COLOR_HIDDEN_MULTIPLIER); + auto colorActiveHidden = to_imvec4(to_vec4(colorActive) * COLOR_HIDDEN_MULTIPLIER); + auto colorHoveredHidden = to_imvec4(to_vec4(colorHidden) * COLOR_HIDDEN_MULTIPLIER); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, style.ItemSpacing); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); + + auto childSize = ImVec2(width, ImGui::GetTextLineHeightWithSpacing() + (ImGui::GetStyle().WindowPadding.y * 2)); + + ImGui::PopStyleVar(2); + + ImGui::PushID(index); + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2()); + + if (ImGui::BeginChild("##Frames Child", childSize, ImGuiChildFlags_Borders)) + { + auto length = animation ? animation->frameNum : anm2.animations.length(); + auto frameSize = ImVec2(ImGui::GetTextLineHeight(), ImGui::GetContentRegionAvail().y); + auto framesSize = ImVec2(frameSize.x * length, frameSize.y); + auto cursorPos = ImGui::GetCursorPos(); + auto cursorScreenPos = ImGui::GetCursorScreenPos(); + auto border = ImGui::GetStyle().FrameBorderSize; + auto borderLineLength = frameSize.y / 5; + auto scrollX = ImGui::GetScrollX(); + auto available = ImGui::GetContentRegionAvail(); + auto frameMin = std::max(0, (int)std::floor(scrollX / frameSize.x) - 1); + auto frameMax = std::min(anm2::FRAME_NUM_MAX, (int)std::ceil(scrollX + available.x / frameSize.x) + 1); + auto drawList = ImGui::GetWindowDrawList(); + pickerLineDrawList = drawList; + + if (type == anm2::NONE) + { + drawList->AddRectFilled(cursorScreenPos, + ImVec2(cursorScreenPos.x + framesSize.x, cursorScreenPos.y + framesSize.y), + ImGui::GetColorU32(FRAME_TIMELINE_COLOR)); + + for (int i = frameMin; i < frameMax; i++) + { + auto frameScreenPos = ImVec2(cursorScreenPos.x + frameSize.x * (float)i, cursorScreenPos.y); + + drawList->AddRect(frameScreenPos, ImVec2(frameScreenPos.x + border, frameScreenPos.y + borderLineLength), + ImGui::GetColorU32(FRAME_BORDER_COLOR)); + + drawList->AddRect(ImVec2(frameScreenPos.x, frameScreenPos.y + frameSize.y - borderLineLength), + ImVec2(frameScreenPos.x + border, frameScreenPos.y + frameSize.y), + ImGui::GetColorU32(FRAME_BORDER_COLOR)); + + if (i % FRAME_MULTIPLE == 0) + { + auto string = std::to_string(i); + auto textSize = ImGui::CalcTextSize(string.c_str()); + auto textPos = ImVec2(frameScreenPos.x + (frameSize.x - textSize.x) / 2, + frameScreenPos.y + (frameSize.y - textSize.y) / 2); + + drawList->AddRectFilled(frameScreenPos, + ImVec2(frameScreenPos.x + frameSize.x, frameScreenPos.y + frameSize.y), + ImGui::GetColorU32(FRAME_MULTIPLE_OVERLAY_COLOR)); + + drawList->AddText(textPos, ImGui::GetColorU32(TEXT_MULTIPLE_COLOR), string.c_str()); + } + } + + if (ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) && ImGui::IsMouseDown(0)) + isDragging = true; + + if (isDragging) + { + auto childPos = ImGui::GetWindowPos(); + auto mousePos = ImGui::GetIO().MousePos; + auto localMousePos = ImVec2(mousePos.x - childPos.x, mousePos.y - childPos.y); + playback.time = floorf(localMousePos.x / frameSize.x); + reference.frameTime = playback.time; + } + + playback.clamp(settings.playbackIsClampPlayhead ? length : anm2::FRAME_NUM_MAX); + + if (ImGui::IsMouseReleased(0)) isDragging = false; + + ImGui::SetCursorPos(ImVec2(cursorPos.x + frameSize.x * floorf(playback.time), cursorPos.y)); + ImGui::Image(resources.icons[icon::PLAYHEAD].id, frameSize); + } + else if (animation) + { + anm2::Reference itemReference{reference.animationIndex, type, id}; + + ImGui::PushStyleColor(ImGuiCol_ButtonActive, isVisible ? colorActive : colorActiveHidden); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, isVisible ? colorHovered : colorHoveredHidden); + + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); + + float frameTime{}; + + for (auto [i, frame] : std::views::enumerate(item->frames)) + { + ImGui::PushID(i); + + auto frameReference = + anm2::Reference(itemReference.animationIndex, itemReference.itemType, itemReference.itemID, i); + auto isSelected = reference == frameReference; + vec2 frameMin = {frameTime * frameSize.x, cursorPos.y}; + vec2 frameMax = {frameMin.x + frame.delay * frameSize.x, frameMin.y + frameSize.y}; + auto buttonSize = to_imvec2(frameMax - frameMin); + auto buttonPos = ImVec2(cursorPos.x + frameMin.x, cursorPos.y); + ImGui::SetCursorPos(buttonPos); + ImGui::PushStyleColor(ImGuiCol_Button, isSelected && isVisible ? colorActive + : isSelected && !isVisible ? colorActiveHidden + : isVisible ? color + : colorHidden); + if (ImGui::Button("##Frame Button", buttonSize)) + { + if (type == anm2::LAYER) + { + document.spritesheet.reference = anm2.content.layers[id].spritesheetID; + document.layer.selection = {id}; + } + reference = frameReference; + reference.frameTime = frameTime; + + if (ImGui::IsKeyDown(ImGuiMod_Alt)) + DOCUMENT_EDIT(document, "Frame Interpolation", Document::FRAMES, + frame.isInterpolated = !frame.isInterpolated); + } + if (ImGui::IsItemHovered()) hoveredFrame = frameReference; + ImGui::PopStyleColor(); + auto icon = type == anm2::TRIGGER ? icon::TRIGGER + : frame.isInterpolated ? icon::INTERPOLATED + : icon::UNINTERPOLATED; + auto iconPos = ImVec2(cursorPos.x + (frameTime * frameSize.x), + cursorPos.y + (frameSize.y / 2) - (icon_size_get().y / 2)); + ImGui::SetCursorPos(iconPos); + ImGui::Image(resources.icons[icon].id, icon_size_get()); + + frameTime += frame.delay; + + ImGui::PopID(); + } + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + } + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + + index++; + ImGui::PopID(); + }; + + auto frames_child = [&]() + { + auto& anm2 = document.anm2; + + auto itemsChildWidth = ImGui::GetTextLineHeightWithSpacing() * 15; + + auto cursorPos = ImGui::GetCursorPos(); + ImGui::SetCursorPos(ImVec2(cursorPos.x + itemsChildWidth, cursorPos.y)); + + auto framesChildSize = ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y); + + if (ImGui::BeginChild("##Frames Child", framesChildSize, ImGuiChildFlags_Borders)) + { + auto viewListChildSize = + ImVec2(ImGui::GetContentRegionAvail().x, + ImGui::GetContentRegionAvail().y - ImGui::GetTextLineHeightWithSpacing() - style.ItemSpacing.y * 2); + + auto childWidth = ImGui::GetContentRegionAvail().x > anm2.animations.length() * ImGui::GetTextLineHeight() + ? ImGui::GetContentRegionAvail().x + : anm2.animations.length() * ImGui::GetTextLineHeight(); + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2()); + if (ImGui::BeginChild("##Frames List Child", viewListChildSize, true, ImGuiWindowFlags_HorizontalScrollbar)) + { + auto cursorScreenPos = ImGui::GetCursorScreenPos(); + + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2()); + if (ImGui::BeginTable("##Frames List Table", 1, + ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) + { + ImGuiWindow* window = ImGui::GetCurrentWindow(); + window->Flags |= ImGuiWindowFlags_NoScrollWithMouse; + + scroll.x = ImGui::GetScrollX(); + scroll.y = ImGui::GetScrollY(); + + isHorizontalScroll = window->ScrollbarX; + + if (isWindowHovered) + { + auto& io = ImGui::GetIO(); + auto lineHeight = ImGui::GetTextLineHeightWithSpacing() * 2; + + scroll.x -= io.MouseWheelH * lineHeight; + scroll.y -= io.MouseWheel * lineHeight; + } + + ImGui::SetScrollX(scroll.x); + ImGui::SetScrollY(scroll.y); + + int index{}; + + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("##Frames"); + + auto frames_child_row = [&](anm2::Type type, int id = -1) + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + frame_child(type, id, index, childWidth); + }; + + frames_child_row(anm2::NONE); + + if (animation) + { + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); + + frames_child_row(anm2::ROOT); + + for (auto& id : animation->layerOrder) + { + if (auto itemPtr = animation->item_get(anm2::LAYER, id); itemPtr) + if (!settings.timelineIsShowUnused && itemPtr->frames.empty()) continue; + + frames_child_row(anm2::LAYER, id); + } + + for (auto& id : animation->nullAnimations | std::views::keys) + { + if (auto itemPtr = animation->item_get(anm2::NULL_, id); itemPtr) + if (!settings.timelineIsShowUnused && itemPtr->frames.empty()) continue; + frames_child_row(anm2::NULL_, id); + } + + frames_child_row(anm2::TRIGGER); + + ImGui::PopStyleVar(); + } + ImGui::EndTable(); + } + + ImDrawList* windowDrawList = ImGui::GetWindowDrawList(); + + auto frameSize = ImVec2(ImGui::GetTextLineHeight(), + ImGui::GetTextLineHeightWithSpacing() + (ImGui::GetStyle().WindowPadding.y * 2)); + auto linePos = ImVec2(cursorScreenPos.x + frameSize.x * floorf(playback.time) + (frameSize.x / 2) - scroll.x, + cursorScreenPos.y + frameSize.y); + auto lineSize = + ImVec2((PLAYHEAD_LINE_THICKNESS / 2.0f), + viewListChildSize.y - frameSize.y - (isHorizontalScroll ? ImGui::GetStyle().ScrollbarSize : 0.0f)); + + auto rectMin = windowDrawList->GetClipRectMin(); + auto rectMax = windowDrawList->GetClipRectMax(); + pickerLineDrawList->PushClipRect(rectMin, rectMax); + pickerLineDrawList->AddRectFilled(linePos, ImVec2(linePos.x + lineSize.x, linePos.y + lineSize.y), + ImGui::GetColorU32(PLAYHEAD_LINE_COLOR)); + pickerLineDrawList->PopClipRect(); + + ImGui::PopStyleVar(); + + context_menu(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.WindowPadding); + + ImGui::SetCursorPos( + ImVec2(ImGui::GetStyle().WindowPadding.x, ImGui::GetCursorPos().y + ImGui::GetStyle().ItemSpacing.y)); + + auto widgetSize = widget_size_with_row_get(10); + + ImGui::BeginDisabled(!animation); + { + auto label = playback.isPlaying ? "Pause" : "Play"; + auto tooltip = playback.isPlaying ? "Pause the animation." : "Play the animation."; + + shortcut(settings.shortcutPlayPause); + if (ImGui::Button(label, widgetSize)) playback.toggle(); + set_item_tooltip_shortcut(tooltip, settings.shortcutPlayPause); + + ImGui::SameLine(); + + auto itemPtr = document.item_get(); + + ImGui::BeginDisabled(!itemPtr); + { + shortcut(settings.shortcutAdd); + if (ImGui::Button("Insert Frame", widgetSize)) + { + auto insert_frame = [&]() + { + auto frame = document.frame_get(); + if (frame) + { + itemPtr->frames.insert(itemPtr->frames.begin() + reference.frameIndex, *frame); + reference.frameIndex++; + } + else if (!itemPtr->frames.empty()) + { + auto lastFrame = itemPtr->frames.back(); + itemPtr->frames.emplace_back(lastFrame); + reference.frameIndex = static_cast(itemPtr->frames.size()) - 1; + } + }; + + DOCUMENT_EDIT(document, "Insert Frame", Document::FRAMES, insert_frame()); + } + set_item_tooltip_shortcut("Insert a frame, based on the current selection.", settings.shortcutAdd); + + ImGui::SameLine(); + + ImGui::BeginDisabled(!document.frame_get()); + { + shortcut(settings.shortcutRemove); + if (ImGui::Button("Delete Frame", widgetSize)) + { + auto delete_frame = [&]() + { + itemPtr->frames.erase(itemPtr->frames.begin() + reference.frameIndex); + reference.frameIndex = glm::max(-1, --reference.frameIndex); + }; + + DOCUMENT_EDIT(document, "Delete Frame(s)", Document::FRAMES, delete_frame()); + } + set_item_tooltip_shortcut("Delete the selected frames.", settings.shortcutRemove); + + ImGui::SameLine(); + + if (ImGui::Button("Bake", widgetSize)) bakePopup.open(); + ImGui::SetItemTooltip("Turn interpolated frames into uninterpolated ones."); + } + ImGui::EndDisabled(); + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + + if (ImGui::Button("Fit Animation Length", widgetSize)) animation->frameNum = animation->length(); + ImGui::SetItemTooltip("The animation length will be set to the effective length of the animation."); + + ImGui::SameLine(); + + ImGui::SetNextItemWidth(widgetSize.x); + ImGui::InputInt("Animation Length", animation ? &animation->frameNum : &dummy_value(), STEP, STEP_FAST, + !animation ? ImGuiInputTextFlags_DisplayEmptyRefVal : 0); + if (animation) animation->frameNum = clamp(animation->frameNum, anm2::FRAME_NUM_MIN, anm2::FRAME_NUM_MAX); + ImGui::SetItemTooltip("Set the animation's length."); + + ImGui::SameLine(); + + ImGui::SetNextItemWidth(widgetSize.x); + ImGui::Checkbox("Loop", animation ? &animation->isLoop : &dummy_value()); + ImGui::SetItemTooltip("Toggle the animation looping."); + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + + ImGui::SetNextItemWidth(widgetSize.x); + ImGui::InputInt("FPS", &anm2.info.fps, 1, 5); + anm2.info.fps = clamp(anm2.info.fps, anm2::FPS_MIN, anm2::FPS_MAX); + ImGui::SetItemTooltip("Set the FPS of all animations."); + + ImGui::SameLine(); + + ImGui::SetNextItemWidth(widgetSize.x); + input_text_string("Author", &anm2.info.createdBy); + ImGui::SetItemTooltip("Set the author of the document."); + + ImGui::SameLine(); + + ImGui::SetNextItemWidth(widgetSize.x); + ImGui::Checkbox("Sound", &settings.timelineIsSound); + ImGui::SetItemTooltip("Toggle sounds playing with triggers.\nBind sounds to events in the Events window."); + + ImGui::PopStyleVar(); + } + ImGui::EndChild(); + + ImGui::SetCursorPos(cursorPos); + }; + + auto popups_fn = [&]() + { + auto item_properties_reset = [&]() + { + addItemName.clear(); + addItemSpritesheetID = {}; + addItemID = -1; + isUnusedItemsSet = false; + }; + + auto& anm2 = document.anm2; + + propertiesPopup.trigger(); + + if (ImGui::BeginPopupModal(propertiesPopup.label, &propertiesPopup.isOpen, ImGuiWindowFlags_NoResize)) + { + auto item_properties_close = [&]() + { + item_properties_reset(); + propertiesPopup.close(); + }; + + auto& type = settings.timelineAddItemType; + auto& locale = settings.timelineAddItemLocale; + auto& source = settings.timelineAddItemSource; + + if (!isUnusedItemsSet) + { + unusedItems = type == anm2::LAYER ? anm2.layers_unused(reference) + : type == anm2::NULL_ ? anm2.nulls_unused(reference) + : std::set{}; + + isUnusedItemsSet = true; + } + + auto footerSize = footer_size_get(); + auto optionsSize = child_size_get(11); + auto itemsSize = ImVec2(0, ImGui::GetContentRegionAvail().y - + (optionsSize.y + footerSize.y + ImGui::GetStyle().ItemSpacing.y * 4)); + if (ImGui::BeginChild("Options", optionsSize, ImGuiChildFlags_Borders)) + { + ImGui::SeparatorText("Type"); + + auto size = ImVec2(ImGui::GetContentRegionAvail().x * 0.5f, ImGui::GetFrameHeightWithSpacing()); + + if (ImGui::BeginChild("Type Layer", size)) + { + ImGui::RadioButton("Layer", &type, anm2::LAYER); + ImGui::SetItemTooltip( + "Layers are a basic visual element in an animation, used for displaying spritesheets."); + } + ImGui::EndChild(); + + ImGui::SameLine(); + + if (ImGui::BeginChild("Type Null", size)) + { + ImGui::RadioButton("Null", &type, anm2::NULL_); + ImGui::SetItemTooltip( + "Nulls are invisible elements in an animation, used for interfacing with a game engine."); + } + ImGui::EndChild(); + + ImGui::SeparatorText("Source"); + + bool isNewOnly = unusedItems.empty(); + if (isNewOnly) source = source::NEW; + + if (ImGui::BeginChild("Source New", size)) + { + ImGui::RadioButton("New", &source, source::NEW); + ImGui::SetItemTooltip("Create a new item to be used."); + } + ImGui::EndChild(); + + ImGui::SameLine(); + + if (ImGui::BeginChild("Source Existing", size)) + { + ImGui::BeginDisabled(isNewOnly); + ImGui::RadioButton("Existing", &source, source::EXISTING); + ImGui::EndDisabled(); + ImGui::SetItemTooltip("Use a pre-existing, presently unused item."); + } + ImGui::EndChild(); + + ImGui::SeparatorText("Locale"); + + if (ImGui::BeginChild("Locale Global", size)) + { + ImGui::RadioButton("Global", &locale, locale::GLOBAL); + ImGui::SetItemTooltip("The item will be inserted into all animations, if not already present."); + } + ImGui::EndChild(); + + ImGui::SameLine(); + + if (ImGui::BeginChild("Locale Local", size)) + { + ImGui::RadioButton("Local", &locale, locale::LOCAL); + ImGui::SetItemTooltip("The item will only be inserted into this animation."); + } + ImGui::EndChild(); + + ImGui::SeparatorText("Options"); + + ImGui::BeginDisabled(source == source::EXISTING); + { + input_text_string("Name", &addItemName); + ImGui::SetItemTooltip("Set the item's name."); + ImGui::BeginDisabled(type != anm2::LAYER); + { + combo_negative_one_indexed("Spritesheet", &addItemSpritesheetID, document.spritesheet.labels); + ImGui::SetItemTooltip("Set the layer item's spritesheet."); + } + ImGui::EndDisabled(); + } + ImGui::EndDisabled(); + } + ImGui::EndChild(); + + if (ImGui::BeginChild("Items", itemsSize, ImGuiChildFlags_Borders)) + { + if (animation && source == source::EXISTING) + { + for (auto id : unusedItems) + { + auto isSelected = addItemID == id; + + ImGui::PushID(id); + + if (type == anm2::LAYER) + { + auto& layer = anm2.content.layers[id]; + if (ImGui::Selectable( + std::format("#{} {} (Spritesheet: #{})", id, layer.name, layer.spritesheetID).c_str(), + isSelected)) + addItemID = id; + } + else if (type == anm2::NULL_) + { + auto& null = anm2.content.nulls[id]; + if (ImGui::Selectable(std::format("#{} {}", id, null.name).c_str(), isSelected)) addItemID = id; + } + + ImGui::PopID(); + } + } + } + ImGui::EndChild(); + + auto widgetSize = widget_size_with_row_get(2); + + if (ImGui::Button("Add", widgetSize)) + { + anm2::Reference addReference{}; + + document.snapshot("Add Item"); + if (type == anm2::LAYER) + addReference = anm2.layer_animation_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName, + addItemSpritesheetID - 1, (locale::Type)locale); + else if (type == anm2::NULL_) + addReference = anm2.null_animation_add({reference.animationIndex, anm2::LAYER, addItemID}, addItemName, + (locale::Type)locale); + + document.change(Document::ITEMS); + + reference = addReference; + + item_properties_close(); + } + ImGui::SetItemTooltip("Add the item, with the settings specified."); + + ImGui::SameLine(); + + if (ImGui::Button("Cancel", widgetSize)) item_properties_close(); + ImGui::SetItemTooltip("Cancel adding an item."); + + ImGui::EndPopup(); + } + + bakePopup.trigger(); + + if (ImGui::BeginPopupModal(bakePopup.label, &bakePopup.isOpen, ImGuiWindowFlags_NoResize)) + { + auto& interval = settings.bakeInterval; + auto& isRoundRotation = settings.bakeIsRoundRotation; + auto& isRoundScale = settings.bakeIsRoundScale; + + auto frame = document.frame_get(); + + input_int_range("Interval", interval, anm2::FRAME_DELAY_MIN, frame ? frame->delay : anm2::FRAME_DELAY_MIN); + ImGui::SetItemTooltip("Set the maximum delay of each frame that will be baked."); + + ImGui::Checkbox("Round Rotation", &isRoundRotation); + ImGui::SetItemTooltip("Rotation will be rounded to the nearest whole number."); + + ImGui::Checkbox("Round Scale", &isRoundScale); + ImGui::SetItemTooltip("Scale will be rounded to the nearest whole number."); + + auto widgetSize = widget_size_with_row_get(2); + + if (ImGui::Button("Bake", widgetSize)) + { + if (auto itemPtr = document.item_get()) + DOCUMENT_EDIT(document, "Bake Frames", Document::FRAMES, + itemPtr->frames_bake(reference.frameIndex, interval, isRoundScale, isRoundRotation)); + bakePopup.close(); + } + ImGui::SetItemTooltip("Bake the selected frame(s) with the options selected."); + + ImGui::SameLine(); + + if (ImGui::Button("Cancel", widgetSize)) bakePopup.close(); + ImGui::SetItemTooltip("Cancel baking frames."); + + ImGui::EndPopup(); + } + }; + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); if (ImGui::Begin("Timeline", &settings.windowIsTimeline)) { isWindowHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); - frames_child(document, animation, settings, resources, clipboard); - items_child(manager, document, animation, settings, resources, clipboard); + frames_child(); + items_child(); } ImGui::PopStyleVar(); ImGui::End(); - popups(document, animation, settings); + popups_fn(); if (shortcut(settings.shortcutPlayPause, shortcut::GLOBAL)) playback.toggle(); @@ -1118,4 +1008,4 @@ namespace anm2ed::imgui } } } -} \ No newline at end of file +} diff --git a/src/imgui/window/timeline.h b/src/imgui/window/timeline.h index 94a9552..e1b41cc 100644 --- a/src/imgui/window/timeline.h +++ b/src/imgui/window/timeline.h @@ -1,7 +1,6 @@ #pragma once #include "clipboard.h" -#include "document.h" #include "manager.h" #include "resources.h" #include "settings.h" @@ -25,14 +24,6 @@ namespace anm2ed::imgui ImDrawList* pickerLineDrawList{}; ImGuiStyle style{}; - void context_menu(Document&, Settings&, Clipboard&); - void item_child(Manager&, Document&, anm2::Animation*, Settings&, Resources&, Clipboard&, anm2::Type, int, int&); - void items_child(Manager&, Document&, anm2::Animation*, Settings&, Resources&, Clipboard&); - void frame_child(Document&, anm2::Animation*, Settings&, Resources&, Clipboard&, anm2::Type, int, int&, float); - void frames_child(Document&, anm2::Animation*, Settings&, Resources&, Clipboard&); - - void popups(Document&, anm2::Animation*, Settings&); - public: void update(Manager&, Settings&, Resources&, Clipboard&); }; diff --git a/src/loader.cpp b/src/loader.cpp index 5113398..f5ae413 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -1,5 +1,7 @@ #include "loader.h" +#include + #include #include @@ -8,11 +10,57 @@ #include "filesystem_.h" #include "log.h" +#include "socket.h" + using namespace anm2ed::types; using namespace anm2ed::util; namespace anm2ed { + constexpr auto SOCKET_ADDRESS = "127.0.0.1"; + constexpr auto SOCKET_PORT = 11414; + + namespace + { + bool socket_paths_send(Socket& socket, const std::vector& paths) + { + uint32_t count = htonl(static_cast(paths.size())); + if (!socket.send(&count, sizeof(count))) return false; + + for (const auto& path : paths) + { + uint32_t length = htonl(static_cast(path.size())); + if (!socket.send(&length, sizeof(length))) return false; + if (!path.empty() && !socket.send(path.data(), path.size())) return false; + } + + return true; + } + + std::vector socket_paths_receive(Socket& socket) + { + uint32_t count{}; + if (!socket.receive(&count, sizeof(count))) return {}; + count = ntohl(count); + + std::vector paths; + paths.reserve(count); + + for (uint32_t i = 0; i < count; ++i) + { + uint32_t length{}; + if (!socket.receive(&length, sizeof(length))) return {}; + length = ntohl(length); + + std::string path(length, '\0'); + if (length > 0 && !socket.receive(path.data(), length)) return {}; + paths.emplace_back(std::move(path)); + } + + return paths; + } + } + std::string Loader::settings_path() { return filesystem::path_preferences_get() + "settings.ini"; @@ -23,6 +71,45 @@ namespace anm2ed for (int i = 1; i < argc; i++) arguments.emplace_back(argv[i]); + Socket testSocket; + if (!testSocket.open(SERVER)) + logger.warning(std::format("Failed to open socket; single instancing will not work.")); + + bool isPrimaryInstance = false; + + if (testSocket.bind({SOCKET_ADDRESS, SOCKET_PORT})) + { + socket = std::move(testSocket); + + if (!socket.listen()) + logger.warning("Could not listen on socket; single instancing disabled."); + else + { + isPrimaryInstance = true; + isSocketThread = true; + logger.info(std::format("Opened socket at {}:{}", SOCKET_ADDRESS, SOCKET_PORT)); + } + } + else + { + logger.info(std::format("Existing instance of program exists; passing arguments...")); + Socket clientSocket; + if (!clientSocket.open(CLIENT)) + logger.warning("Could not open client socket to forward arguments."); + else if (!clientSocket.connect({SOCKET_ADDRESS, SOCKET_PORT})) + logger.warning("Could not connect to existing instance."); + else if (!socket_paths_send(clientSocket, arguments)) + logger.warning("Failed to transfer arguments to existing instance."); + else + logger.info("Sent arguments to existing instance. Exiting."); + + if (!isPrimaryInstance) + { + isError = true; + return; + } + } + settings = Settings(settings_path()); if (!SDL_Init(SDL_INIT_VIDEO)) @@ -93,10 +180,54 @@ namespace anm2ed io.ConfigWindowsMoveFromTitleBarOnly = true; ImGui::LoadIniSettingsFromDisk(settings_path().c_str()); + + if (isSocketThread) + { + isSocketRunning = true; + socketThread = std::thread( + [this]() + { + while (isSocketRunning) + { + auto client = socket.accept(); + if (!client.is_valid()) + { + if (!isSocketRunning) break; + continue; + } + + auto paths = socket_paths_receive(client); + for (auto& path : paths) + { + if (path.empty()) continue; + SDL_Event event{}; + event.type = SDL_EVENT_DROP_FILE; + event.drop.data = SDL_strdup(path.c_str()); + event.drop.windowID = window ? SDL_GetWindowID(window) : 0; + SDL_PushEvent(&event); + } + } + }); + } } Loader::~Loader() { + if (isSocketThread) + { + isSocketRunning = false; + + if (socket.is_valid()) + { + Socket wakeSocket; + if (wakeSocket.open(CLIENT) && wakeSocket.connect({SOCKET_ADDRESS, SOCKET_PORT})) + socket_paths_send(wakeSocket, {}); + } + + socket.close(); + if (socketThread.joinable()) socketThread.join(); + } + if (ImGui::GetCurrentContext()) { settings.save(settings_path(), ImGui::SaveIniSettingsToMemory(nullptr)); @@ -115,4 +246,4 @@ namespace anm2ed SDL_Quit(); } } -} \ No newline at end of file +} diff --git a/src/loader.h b/src/loader.h index 31c751a..3a2c1d5 100644 --- a/src/loader.h +++ b/src/loader.h @@ -1,11 +1,14 @@ #pragma once +#include #include +#include #include #include #include "settings.h" +#include "socket.h" namespace anm2ed { @@ -14,11 +17,15 @@ namespace anm2ed std::string settings_path(); public: + Socket socket{}; + std::thread socketThread{}; + std::atomic_bool isSocketRunning{}; SDL_Window* window{}; SDL_GLContext glContext{}; Settings settings; std::vector arguments; bool isError{}; + bool isSocketThread{}; Loader(int, const char**); ~Loader(); diff --git a/src/manager.cpp b/src/manager.cpp index 086d080..016730e 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -14,20 +14,9 @@ namespace anm2ed { constexpr std::size_t RECENT_LIMIT = 10; - std::filesystem::path Manager::recent_files_path_get() - { - return filesystem::path_preferences_get() + "recent.txt"; - } - - std::filesystem::path Manager::autosave_path_get() - { - return filesystem::path_preferences_get() + "autosave.txt"; - } - - std::filesystem::path Manager::autosave_directory_get() - { - return filesystem::path_preferences_get() + "autosave"; - } + std::filesystem::path Manager::recent_files_path_get() { return filesystem::path_preferences_get() + "recent.txt"; } + std::filesystem::path Manager::autosave_path_get() { return filesystem::path_preferences_get() + "autosave.txt"; } + std::filesystem::path Manager::autosave_directory_get() { return filesystem::path_preferences_get() + "autosave"; } Manager::Manager() { @@ -35,10 +24,7 @@ namespace anm2ed autosave_files_load(); } - Document* Manager::get(int index) - { - return vector::find(documents, index > -1 ? index : selected); - } + Document* Manager::get(int index) { return vector::find(documents, index > -1 ? index : selected); } void Manager::open(const std::string& path, bool isNew, bool isRecent) { @@ -68,10 +54,7 @@ namespace anm2ed toasts.info(std::format("Opened document: {}", path)); } - void Manager::new_(const std::string& path) - { - open(path, true); - } + void Manager::new_(const std::string& path) { open(path, true); } void Manager::save(int index, const std::string& path) { @@ -83,10 +66,7 @@ namespace anm2ed } } - void Manager::save(const std::string& path) - { - save(selected, path); - } + void Manager::save(const std::string& path) { save(selected, path); } void Manager::autosave(Document& document) { @@ -155,15 +135,9 @@ namespace anm2ed } } - void Manager::layer_properties_trigger() - { - layerPropertiesPopup.trigger(); - } + void Manager::layer_properties_trigger() { layerPropertiesPopup.trigger(); } - void Manager::layer_properties_end() - { - layerPropertiesPopup.end(); - } + void Manager::layer_properties_end() { layerPropertiesPopup.end(); } void Manager::layer_properties_close() { @@ -186,15 +160,9 @@ namespace anm2ed } } - void Manager::null_properties_trigger() - { - nullPropertiesPopup.trigger(); - } + void Manager::null_properties_trigger() { nullPropertiesPopup.trigger(); } - void Manager::null_properties_end() - { - nullPropertiesPopup.end(); - } + void Manager::null_properties_end() { nullPropertiesPopup.end(); } void Manager::null_properties_close() { @@ -313,8 +281,5 @@ namespace anm2ed autosave_files_write(); } - Manager::~Manager() - { - autosave_files_clear(); - } + Manager::~Manager() { autosave_files_clear(); } } diff --git a/src/render.cpp b/src/render.cpp index 3258c2f..5d43203 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -1,7 +1,10 @@ #include "render.h" +#include #include +#include #include +#include #ifdef _WIN32 #include "util.h" @@ -9,7 +12,7 @@ #define PCLOSE _pclose #define PWRITE_MODE "wb" #define PREAD_MODE "r" -#else +#elif __unix__ #define POPEN popen #define PCLOSE pclose #define PWRITE_MODE "w" @@ -25,44 +28,87 @@ namespace anm2ed { constexpr auto FFMPEG_POPEN_ERROR = "popen() (for FFmpeg) failed!\n{}"; - constexpr auto GIF_FORMAT = "\"{0}\" -y " - "-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 " - "-lavfi \"split[s0][s1];" - "[s0]palettegen=stats_mode=full[p];" - "[s1][p]paletteuse=dither=floyd_steinberg\" " - "-loop 0 \"{4}\""; - - constexpr auto WEBM_FORMAT = "\"{0}\" -y " - "-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 " - "-c:v libvpx-vp9 -crf 30 -b:v 0 -pix_fmt yuva420p -row-mt 1 -threads 0 -speed 2 " - "-auto-alt-ref 0 -an \"{4}\""; - - constexpr auto* MP4_FORMAT = "\"{0}\" -y " - "-f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0 " - "-vf \"format=yuv420p,scale=trunc(iw/2)*2:trunc(ih/2)*2\" " - "-c:v libx265 -crf 20 -preset slow " - "-tag:v hvc1 -movflags +faststart -an \"{4}\""; - bool animation_render(const std::string& ffmpegPath, const std::string& path, std::vector& frames, - render::Type type, ivec2 size, int fps) + AudioStream& audioStream, render::Type type, ivec2 size, int fps) { if (frames.empty() || size.x <= 0 || size.y <= 0 || fps <= 0 || ffmpegPath.empty() || path.empty()) return false; + std::filesystem::path audioPath{}; + std::string audioInputArguments{}; + std::string audioOutputArguments{"-an"}; std::string command{}; + auto remove_audio_file = [&]() + { + if (!audioPath.empty()) + { + std::error_code ec; + std::filesystem::remove(audioPath, ec); + } + }; + + if (type != render::GIF && !audioStream.stream.empty() && audioStream.spec.freq > 0 && + audioStream.spec.channels > 0) + { + audioPath = std::filesystem::temp_directory_path() / std::format("{}.f32", path); + + std::ofstream audioFile(audioPath, std::ios::binary); + + if (audioFile) + { + auto data = (const char*)audioStream.stream.data(); + auto byteCount = audioStream.stream.size() * sizeof(float); + audioFile.write(data, byteCount); + audioFile.close(); + + audioInputArguments = std::format("-f f32le -ar {0} -ac {1} -i \"{2}\"", audioStream.spec.freq, + audioStream.spec.channels, audioPath.string()); + + switch (type) + { + case render::WEBM: + audioOutputArguments = "-c:a libopus -b:a 160k -shortest"; + break; + case render::MP4: + audioOutputArguments = "-c:a aac -b:a 192k -shortest"; + break; + default: + break; + } + } + else + { + logger.warning("Failed to open temporary audio file; exporting video without audio."); + remove_audio_file(); + } + } + + command = std::format("\"{0}\" -y -f rawvideo -pix_fmt rgba -s {1}x{2} -r {3} -i pipe:0", ffmpegPath, size.x, + size.y, fps); + + if (!audioInputArguments.empty()) command += " " + audioInputArguments; + switch (type) { case render::GIF: - command = std::format(GIF_FORMAT, ffmpegPath, size.x, size.y, fps, path); + command += + " -lavfi \"split[s0][s1];[s0]palettegen=stats_mode=full[p];[s1][p]paletteuse=dither=floyd_steinberg\"" + " -loop 0"; + command += std::format(" \"{}\"", path); break; case render::WEBM: - command = std::format(WEBM_FORMAT, ffmpegPath, size.x, size.y, fps, path); + command += " -c:v libvpx-vp9 -crf 30 -b:v 0 -pix_fmt yuva420p -row-mt 1 -threads 0 -speed 2 -auto-alt-ref 0"; + if (!audioOutputArguments.empty()) command += " " + audioOutputArguments; + command += std::format(" \"{}\"", path); break; case render::MP4: - command = std::format(MP4_FORMAT, ffmpegPath, size.x, size.y, fps, path); + command += " -vf \"format=yuv420p,scale=trunc(iw/2)*2:trunc(ih/2)*2\" -c:v libx265 -crf 20 -preset slow" + " -tag:v hvc1 -movflags +faststart"; + if (!audioOutputArguments.empty()) command += " " + audioOutputArguments; + command += std::format(" \"{}\"", path); break; default: - break; + return false; } #if _WIN32 @@ -75,6 +121,7 @@ namespace anm2ed if (!fp) { + remove_audio_file(); logger.error(std::format(FFMPEG_POPEN_ERROR, strerror(errno))); return false; } @@ -85,12 +132,14 @@ namespace anm2ed if (fwrite(frame.pixels.data(), 1, frameSize, fp) != frameSize) { + remove_audio_file(); PCLOSE(fp); return false; } } auto code = PCLOSE(fp); + remove_audio_file(); return (code == 0); } -} \ No newline at end of file +} diff --git a/src/render.h b/src/render.h index 22f42d8..22df9a7 100644 --- a/src/render.h +++ b/src/render.h @@ -1,5 +1,6 @@ #pragma once +#include "audio_stream.h" #include "texture.h" namespace anm2ed::render @@ -33,6 +34,6 @@ namespace anm2ed::render namespace anm2ed { - bool animation_render(const std::string&, const std::string&, std::vector&, render::Type, - glm::ivec2, int); + bool animation_render(const std::string&, const std::string&, std::vector&, AudioStream&, + render::Type, glm::ivec2, int); } \ No newline at end of file diff --git a/src/resource/audio_stream.cpp b/src/resource/audio_stream.cpp new file mode 100644 index 0000000..8c2c333 --- /dev/null +++ b/src/resource/audio_stream.cpp @@ -0,0 +1,26 @@ +#include "audio_stream.h" + +namespace anm2ed +{ + void AudioStream::callback(void* userData, MIX_Mixer* mixer, const SDL_AudioSpec* spec, float* pcm, int samples) + { + auto self = (AudioStream*)userData; + self->stream.insert(self->stream.end(), pcm, pcm + samples); + } + + AudioStream::AudioStream(MIX_Mixer* mixer) + { + MIX_GetMixerFormat(mixer, &spec); + } + + void AudioStream::capture_begin(MIX_Mixer* mixer) + { + MIX_SetPostMixCallback(mixer, callback, this); + } + + void AudioStream::capture_end(MIX_Mixer* mixer) + { + MIX_SetPostMixCallback(mixer, nullptr, this); + stream.clear(); + } +} \ No newline at end of file diff --git a/src/resource/audio_stream.h b/src/resource/audio_stream.h new file mode 100644 index 0000000..1cbb90f --- /dev/null +++ b/src/resource/audio_stream.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace anm2ed +{ + class AudioStream + { + static void callback(void*, MIX_Mixer*, const SDL_AudioSpec*, float*, int); + + public: + std::vector stream{}; + SDL_AudioSpec spec{}; + + AudioStream(MIX_Mixer*); + void capture_begin(MIX_Mixer*); + void capture_end(MIX_Mixer*); + }; +} \ No newline at end of file diff --git a/src/resource/icon.h b/src/resource/icon.h index 4101ddf..784faf3 100644 --- a/src/resource/icon.h +++ b/src/resource/icon.h @@ -153,7 +153,7 @@ namespace anm2ed::resource::icon )"; -#define LIST \ +#define SVG_LIST \ X(NONE, NONE_DATA, SIZE_SMALL) \ X(FILE, FILE_DATA, SIZE_NORMAL) \ X(FOLDER, FOLDER_DATA, SIZE_NORMAL) \ @@ -195,7 +195,7 @@ namespace anm2ed::resource::icon enum Type { #define X(name, data, size) name, - LIST + SVG_LIST #undef X COUNT }; @@ -209,7 +209,21 @@ namespace anm2ed::resource::icon const Info ICONS[COUNT] = { #define X(name, data, size) {data, std::strlen(data) - 1, size}, - LIST + SVG_LIST #undef X }; + + constexpr uint8_t PROGRAM[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, 0x40, 0x08, 0x04, 0x00, 0x00, 0x00, 0x00, 0x60, 0xb9, 0x55, 0x00, 0x00, 0x00, 0x09, 0x70, + 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, + 0x8b, 0x49, 0x44, 0x41, 0x54, 0x68, 0xde, 0xed, 0xd7, 0x3d, 0x0e, 0x80, 0x20, 0x0c, 0x86, 0x61, 0x7b, 0x53, 0x3c, + 0x99, 0xbd, 0x69, 0x1d, 0x8c, 0x83, 0x4d, 0xd0, 0x44, 0x29, 0x7f, 0xbe, 0x5d, 0x08, 0x30, 0xf4, 0x19, 0x80, 0x2f, + 0xc8, 0xd2, 0xb8, 0x04, 0xc0, 0xb4, 0x80, 0x64, 0xc7, 0xb8, 0xf9, 0x86, 0x02, 0x60, 0x36, 0x80, 0xd9, 0x75, 0xbe, + 0xba, 0x7d, 0x00, 0xf3, 0x00, 0x72, 0x8d, 0x7c, 0x83, 0x73, 0x5d, 0xa5, 0xf0, 0x43, 0x04, 0xa0, 0x1a, 0x20, 0xaa, + 0x11, 0x80, 0xf1, 0x01, 0x1a, 0x14, 0x5b, 0x00, 0xfa, 0x03, 0x24, 0xbb, 0x0f, 0x93, 0xa7, 0xb0, 0xf9, 0x1c, 0x46, + 0x00, 0x00, 0x00, 0xe8, 0x06, 0xa0, 0xa5, 0x3f, 0x2a, 0x99, 0x07, 0x0d, 0x40, 0xbf, 0x80, 0xa8, 0x02, 0x30, 0x0e, + 0x40, 0xdf, 0x1e, 0xb2, 0x56, 0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x07, 0x90, 0x86, 0xff, 0x05, 0xd4, + 0x2e, 0x00, 0x00, 0x00, 0x34, 0x07, 0xec, 0x94, 0x51, 0xac, 0x41, 0x55, 0x6e, 0xe4, 0x26, 0x00, 0x00, 0x00, 0x00, + 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82}; } \ No newline at end of file diff --git a/src/resource/texture.cpp b/src/resource/texture.cpp index 89170ec..fc75619 100644 --- a/src/resource/texture.cpp +++ b/src/resource/texture.cpp @@ -28,15 +28,9 @@ using namespace glm; namespace anm2ed::resource { - bool Texture::is_valid() - { - return id != 0; - } + bool Texture::is_valid() { return id != 0; } - size_t Texture::pixel_size_get() - { - return size.x * size.y * CHANNELS; - } + size_t Texture::pixel_size_get() { return size.x * size.y * CHANNELS; } void Texture::upload(const uint8_t* data) { @@ -66,15 +60,9 @@ namespace anm2ed::resource if (is_valid()) glDeleteTextures(1, &id); } - Texture::Texture(const Texture& other) - { - *this = other; - } + Texture::Texture(const Texture& other) { *this = other; } - Texture::Texture(Texture&& other) - { - *this = std::move(other); - } + Texture::Texture(Texture&& other) { *this = std::move(other); } Texture& Texture::operator=(const Texture& other) // Copy { diff --git a/src/resource/texture.h b/src/resource/texture.h index c73e138..87d9894 100644 --- a/src/resource/texture.h +++ b/src/resource/texture.h @@ -26,8 +26,8 @@ namespace anm2ed::resource size_t pixel_size_get(); void upload(); void upload(const uint8_t*); - Texture(); + Texture(); ~Texture(); Texture(const Texture&); Texture(Texture&&); diff --git a/src/settings.cpp b/src/settings.cpp index 84e22f8..7429bbb 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -10,97 +10,97 @@ namespace anm2ed { constexpr auto IMGUI_DEFAULT = R"( # Dear ImGui -[Window][## Window] -Pos=0,32 -Size=1600,868 +[Window][##DockSpace] +Pos=0,54 +Size=1918,1010 Collapsed=0 -[Window][Debug##Default] -Pos=60,60 -Size=400,400 -Collapsed=0 - -[Window][Tools] -Pos=8,40 -Size=38,516 -Collapsed=0 -DockId=0x0000000B,0 - -[Window][Animations] -Pos=1289,307 -Size=303,249 -Collapsed=0 -DockId=0x0000000A,0 - -[Window][Events] -Pos=957,264 -Size=330,292 -Collapsed=0 -DockId=0x00000008,2 - -[Window][Spritesheets] -Pos=1289,40 -Size=303,265 -Collapsed=0 -DockId=0x00000009,0 - [Window][Animation Preview] -Pos=48,40 -Size=907,516 +Pos=60,62 +Size=983,691 +Collapsed=0 +DockId=0x00000003,1 + +[Window][Animations] +Pos=1451,494 +Size=459,259 Collapsed=0 DockId=0x0000000C,0 -[Window][Spritesheet Editor] -Pos=48,40 -Size=907,516 -Collapsed=0 -DockId=0x0000000C,1 - -[Window][Timeline] -Pos=8,558 -Size=1584,334 -Collapsed=0 -DockId=0x00000004,0 - -[Window][Frame Properties] -Pos=957,40 -Size=330,222 -Collapsed=0 -DockId=0x00000007,0 - -[Window][Onionskin] -Pos=957,264 -Size=330,292 -Collapsed=0 -DockId=0x00000008,3 - -[Window][Layers] -Pos=957,264 -Size=330,292 +[Window][Events] +Pos=1045,463 +Size=404,290 Collapsed=0 DockId=0x00000008,0 -[Window][Nulls] -Pos=957,264 -Size=330,292 +[Window][Frame Properties] +Pos=1045,62 +Size=404,399 +Collapsed=0 +DockId=0x00000007,0 + +[Window][Layers] +Pos=1045,463 +Size=404,290 Collapsed=0 DockId=0x00000008,1 +[Window][Nulls] +Pos=1045,463 +Size=404,290 +Collapsed=0 +DockId=0x00000008,2 + +[Window][Onionskin] +Pos=8,755 +Size=1902,301 +Collapsed=0 +DockId=0x00000006,1 + +[Window][Spritesheet Editor] +Pos=60,62 +Size=983,691 +Collapsed=0 +DockId=0x00000003,0 + +[Window][Spritesheets] +Pos=1451,62 +Size=459,430 +Collapsed=0 +DockId=0x0000000B,0 + +[Window][Tools] +Pos=8,62 +Size=50,691 +Collapsed=0 +DockId=0x00000001,0 + +[Window][Timeline] +Pos=8,755 +Size=1902,301 +Collapsed=0 +DockId=0x00000006,0 + +[Window][Sounds] +Pos=1045,463 +Size=404,290 +Collapsed=0 +DockId=0x00000008,3 [Docking][Data] -DockSpace ID=0xFC02A410 Window=0x0E46F4F7 Pos=8,40 Size=1584,852 Split=Y - DockNode ID=0x00000003 Parent=0xFC02A410 SizeRef=1902,680 Split=X - DockNode ID=0x00000001 Parent=0x00000003 SizeRef=1017,1016 Split=X Selected=0x024430EF - DockNode ID=0x00000005 Parent=0x00000001 SizeRef=1264,654 Split=X Selected=0x024430EF - DockNode ID=0x0000000B Parent=0x00000005 SizeRef=38,654 Selected=0x18A5FDB9 - DockNode ID=0x0000000C Parent=0x00000005 SizeRef=1224,654 CentralNode=1 Selected=0x024430EF - DockNode ID=0x00000006 Parent=0x00000001 SizeRef=330,654 Split=Y Selected=0x754E368F - DockNode ID=0x00000007 Parent=0x00000006 SizeRef=631,293 Selected=0x754E368F - DockNode ID=0x00000008 Parent=0x00000006 SizeRef=631,385 Selected=0xCD8384B1 - DockNode ID=0x00000002 Parent=0x00000003 SizeRef=303,1016 Split=Y Selected=0x4EFD0020 - DockNode ID=0x00000009 Parent=0x00000002 SizeRef=634,349 Selected=0x4EFD0020 - DockNode ID=0x0000000A Parent=0x00000002 SizeRef=634,329 Selected=0xC1986EE2 - DockNode ID=0x00000004 Parent=0xFC02A410 SizeRef=1902,334 Selected=0x4F89F0DC +DockSpace ID=0x123F8F08 Window=0x6D581B32 Pos=8,62 Size=1902,994 Split=Y Selected=0x4EFD0020 + DockNode ID=0x00000005 Parent=0x123F8F08 SizeRef=1910,691 Split=X + DockNode ID=0x00000001 Parent=0x00000005 SizeRef=50,994 Selected=0x18A5FDB9 + DockNode ID=0x00000002 Parent=0x00000005 SizeRef=1850,994 Split=X Selected=0x4EFD0020 + DockNode ID=0x00000003 Parent=0x00000002 SizeRef=983,994 Selected=0x024430EF + DockNode ID=0x00000004 Parent=0x00000002 SizeRef=865,994 Split=X Selected=0x4EFD0020 + DockNode ID=0x00000009 Parent=0x00000004 SizeRef=404,497 Split=Y Selected=0xCD8384B1 + DockNode ID=0x00000007 Parent=0x00000009 SizeRef=181,399 Selected=0x754E368F + DockNode ID=0x00000008 Parent=0x00000009 SizeRef=181,290 Selected=0x8A65D963 + DockNode ID=0x0000000A Parent=0x00000004 SizeRef=459,497 Split=Y Selected=0x4EFD0020 + DockNode ID=0x0000000B Parent=0x0000000A SizeRef=710,430 CentralNode=1 Selected=0x4EFD0020 + DockNode ID=0x0000000C Parent=0x0000000A SizeRef=710,259 Selected=0xC1986EE2 + DockNode ID=0x00000006 Parent=0x123F8F08 SizeRef=1910,301 Selected=0x4F89F0DC )"; Settings::Settings(const std::string& path) diff --git a/src/settings.h b/src/settings.h index e25bfd8..db1ac02 100644 --- a/src/settings.h +++ b/src/settings.h @@ -153,8 +153,8 @@ namespace anm2ed /* Symbol / Name / String / Type / Default */ \ X(SHORTCUT_CENTER_VIEW, shortcutCenterView, "Center View", STRING, "Home") \ X(SHORTCUT_FIT, shortcutFit, "Fit", STRING, "F") \ - X(SHORTCUT_ZOOM_IN, shortcutZoomIn, "Zoom In", STRING, "Ctrl++") \ - X(SHORTCUT_ZOOM_OUT, shortcutZoomOut, "Zoom Out", STRING, "Ctrl+-") \ + X(SHORTCUT_ZOOM_IN, shortcutZoomIn, "Zoom In", STRING, "Ctrl+Equal") \ + X(SHORTCUT_ZOOM_OUT, shortcutZoomOut, "Zoom Out", STRING, "Ctrl+Minus") \ X(SHORTCUT_PLAY_PAUSE, shortcutPlayPause, "Play/Pause", STRING, "Space") \ X(SHORTCUT_ONIONSKIN, shortcutOnionskin, "Onionskin", STRING, "O") \ X(SHORTCUT_NEW, shortcutNew, "New", STRING, "Ctrl+N") \ diff --git a/src/snapshots.cpp b/src/snapshots.cpp index 582b8ba..c9df201 100644 --- a/src/snapshots.cpp +++ b/src/snapshots.cpp @@ -4,10 +4,7 @@ using namespace anm2ed::snapshots; namespace anm2ed { - bool SnapshotStack::is_empty() - { - return top == 0; - } + bool SnapshotStack::is_empty() { return top == 0; } void SnapshotStack::push(const Snapshot& snapshot) { @@ -26,10 +23,7 @@ namespace anm2ed return &snapshots[--top]; } - void SnapshotStack::clear() - { - top = 0; - } + void SnapshotStack::clear() { top = 0; } void Snapshots::push(const Snapshot& snapshot) { diff --git a/src/snapshots.h b/src/snapshots.h index 3fefd20..ee7382d 100644 --- a/src/snapshots.h +++ b/src/snapshots.h @@ -25,6 +25,8 @@ namespace anm2ed Storage null{}; Storage sound{}; Storage spritesheet{}; + Storage items{}; + std::map frames{}; std::string message = snapshots::ACTION; }; diff --git a/src/socket.cpp b/src/socket.cpp new file mode 100644 index 0000000..4b408e7 --- /dev/null +++ b/src/socket.cpp @@ -0,0 +1,161 @@ +#include "socket.h" + +namespace anm2ed +{ +#ifdef _WIN32 + namespace + { + struct WSAInitializer + { + WSAInitializer() + { + WSADATA data{}; + WSAStartup(MAKEWORD(2, 2), &data); + } + ~WSAInitializer() { WSACleanup(); } + }; + + WSAInitializer initializer{}; + } +#endif + + Socket::Socket() : handle(SOCKET_INVALID), role(CLIENT) {} + + Socket::Socket(Socket&& other) noexcept : handle(other.handle), role(other.role) { other.handle = SOCKET_INVALID; } + + Socket& Socket::operator=(Socket&& other) noexcept + { + if (this != &other) + { + close(); + handle = other.handle; + role = other.role; + other.handle = SOCKET_INVALID; + } + + return *this; + } + + Socket::~Socket() { close(); } + + bool Socket::open(SocketRole newRole) + { + close(); + role = newRole; + + handle = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + if (!is_valid()) return false; + + if (role == SERVER) + { +#ifdef _WIN32 + BOOL opt = TRUE; +#else + int opt = 1; +#endif + ::setsockopt(handle, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&opt), sizeof(opt)); + } + + return true; + } + + bool Socket::bind(const SocketAddress& address) + { + if (!is_valid()) return false; + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(address.port); + + if (address.host.empty()) + addr.sin_addr.s_addr = htonl(INADDR_ANY); + else + { + if (::inet_pton(AF_INET, address.host.c_str(), &addr.sin_addr) <= 0) return false; + } + + return ::bind(handle, reinterpret_cast(&addr), sizeof(addr)) == 0; + } + + bool Socket::listen() + { + if (!is_valid()) return false; + return ::listen(handle, SOMAXCONN) == 0; + } + + Socket Socket::accept() + { + Socket client{}; + if (!is_valid()) return client; + + auto accepted = ::accept(handle, nullptr, nullptr); + if (accepted == SOCKET_INVALID) return client; + + client.close(); + client.handle = accepted; + client.role = CLIENT; + return client; + } + + bool Socket::connect(const SocketAddress& address) + { + if (!is_valid()) return false; + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(address.port); + + if (::inet_pton(AF_INET, address.host.c_str(), &addr.sin_addr) <= 0) return false; + + return ::connect(handle, reinterpret_cast(&addr), sizeof(addr)) == 0; + } + + bool Socket::send(const void* data, size_t size) + { + if (!is_valid() || !data || size == 0) return false; + + auto bytes = reinterpret_cast(data); + size_t totalSent = 0; + + while (totalSent < size) + { + auto sent = ::send(handle, bytes + totalSent, static_cast(size - totalSent), 0); + if (sent <= 0) return false; + totalSent += static_cast(sent); + } + + return true; + } + + bool Socket::receive(void* buffer, size_t size) + { + if (!is_valid() || !buffer || size == 0) return false; + + auto* bytes = reinterpret_cast(buffer); + size_t totalReceived = 0; + + while (totalReceived < size) + { + auto received = ::recv(handle, bytes + totalReceived, static_cast(size - totalReceived), 0); + if (received <= 0) return false; + totalReceived += static_cast(received); + } + + return true; + } + + void Socket::close() + { + if (!is_valid()) return; + +#ifdef _WIN32 + ::closesocket(handle); +#else + ::close(handle); +#endif + handle = SOCKET_INVALID; + } + + bool Socket::is_valid() const { return handle != SOCKET_INVALID; } +} diff --git a/src/socket.h b/src/socket.h new file mode 100644 index 0000000..ee8e4ea --- /dev/null +++ b/src/socket.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include + +#ifdef _WIN32 + #include + #include +using socket_handle = SOCKET; +constexpr socket_handle SOCKET_INVALID = INVALID_SOCKET; +#else + #include + #include + #include + #include +using socket_handle = int; +constexpr socket_handle SOCKET_INVALID = -1; +#endif + +namespace anm2ed +{ + enum SocketRole + { + SERVER, + CLIENT + }; + + struct SocketAddress + { + std::string host{}; + unsigned short port{}; + }; + + class Socket + { + private: + socket_handle handle; + SocketRole role{}; + + public: + Socket(); + ~Socket(); + + Socket(const Socket&) = delete; + Socket& operator=(const Socket&) = delete; + Socket(Socket&& other) noexcept; + Socket& operator=(Socket&& other) noexcept; + + bool open(SocketRole role); + bool bind(const SocketAddress&); + bool listen(); + Socket accept(); + bool connect(const SocketAddress&); + + bool send(const void*, size_t); + bool receive(void*, size_t); + + void close(); + bool is_valid() const; + }; +} diff --git a/src/state.cpp b/src/state.cpp index d6ea49b..545b835 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -52,7 +52,10 @@ namespace anm2ed { auto droppedFile = event.drop.data; if (filesystem::path_is_extension(droppedFile, "anm2")) + { manager.open(std::string(droppedFile)); + SDL_FlashWindow(window, SDL_FLASH_UNTIL_FOCUSED); + } else if (filesystem::path_is_extension(droppedFile, "png")) { if (auto document = manager.get()) diff --git a/src/util/filesystem_.cpp b/src/util/filesystem_.cpp index 7a665d6..251df42 100644 --- a/src/util/filesystem_.cpp +++ b/src/util/filesystem_.cpp @@ -8,14 +8,37 @@ namespace anm2ed::util::filesystem { - std::string path_preferences_get() + std::string path_pref_get(const char* org, const char* app) { - char* preferencesPath = SDL_GetPrefPath("", "anm2ed"); - std::string preferencesPathString = preferencesPath; - SDL_free(preferencesPath); - return preferencesPathString; + auto path = SDL_GetPrefPath(org, app); + std::string string = path; + SDL_free(path); + return string; } + std::string path_preferences_get() { return path_pref_get(nullptr, "anm2ed"); } + std::string path_base_get() { return std::string(SDL_GetBasePath()); } + std::string path_executable_get() { return std::filesystem::path(path_base_get()) / "anm2ed"; } + +#ifdef __unix__ + std::string path_application_get() + { + return std::filesystem::path(path_pref_get(nullptr, "applications")) / "anm2ed.desktop"; + } + + std::string path_mime_get() + { + return std::filesystem::path(path_pref_get(nullptr, "mime/application")) / "x-anm2+xml.xml"; + } + + std::string path_icon_get() { return std::filesystem::path(path_preferences_get()) / "anm2ed.png"; } + std::string path_icon_file_get() + { + return std::filesystem::path(path_preferences_get()) / "application-x-anm2+xml.png"; + } + +#endif + bool path_is_exist(const std::string& path) { std::error_code errorCode; @@ -49,8 +72,5 @@ namespace anm2ed::util::filesystem std::filesystem::current_path(path); } - WorkingDirectory::~WorkingDirectory() - { - std::filesystem::current_path(previous); - } + WorkingDirectory::~WorkingDirectory() { std::filesystem::current_path(previous); } } \ No newline at end of file diff --git a/src/util/filesystem_.h b/src/util/filesystem_.h index c13fcf6..f9bf2e7 100644 --- a/src/util/filesystem_.h +++ b/src/util/filesystem_.h @@ -5,9 +5,21 @@ namespace anm2ed::util::filesystem { +#ifdef __unix__ + std::string path_application_get(); + std::string path_mime_get(); + std::string path_icon_get(); + std::string path_icon_file_get(); +#endif + + std::string path_pref_get(); std::string path_preferences_get(); + std::string path_base_get(); + std::string path_executable_get(); + bool path_is_exist(const std::string&); bool path_is_extension(const std::string&, const std::string&); + std::filesystem::path path_lower_case_backslash_handle(std::filesystem::path&); class WorkingDirectory