diff --git a/CMakeLists.txt b/CMakeLists.txt index 197d479..2751b53 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,6 +92,8 @@ file(GLOB PROJECT_SRC CONFIGURE_DEPENDS src/imgui/*.h src/imgui/window/*.cpp src/imgui/window/*.h + src/imgui/wizard/*.cpp + src/imgui/wizard/*.h src/util/*.cpp src/util/*.h src/window/*.cpp diff --git a/src/anm2/anm2.cpp b/src/anm2/anm2.cpp index a559ca4..127a241 100644 --- a/src/anm2/anm2.cpp +++ b/src/anm2/anm2.cpp @@ -4,10 +4,11 @@ #include #include -#include "filesystem_.h" +#include "file_.h" #include "map_.h" #include "time_.h" #include "vector_.h" +#include "working_directory.h" #include "xml_.h" using namespace tinyxml2; @@ -23,7 +24,7 @@ namespace anm2ed::anm2 { XMLDocument document; - filesystem::File file(path, "rb"); + File file(path, "rb"); if (!file) { if (errorString) *errorString = localize.get(ERROR_FILE_NOT_FOUND); @@ -38,7 +39,7 @@ namespace anm2ed::anm2 return; } - filesystem::WorkingDirectory workingDirectory(path, true); + WorkingDirectory workingDirectory(path, WorkingDirectory::FILE); const XMLElement* element = document.RootElement(); @@ -65,7 +66,7 @@ namespace anm2ed::anm2 XMLDocument document; document.InsertFirstChild(to_element(document)); - filesystem::File file(path, "wb"); + File file(path, "wb"); if (!file) { if (errorString) *errorString = localize.get(ERROR_FILE_NOT_FOUND); diff --git a/src/anm2/anm2_sounds.cpp b/src/anm2/anm2_sounds.cpp index eba4a3e..f2eb492 100644 --- a/src/anm2/anm2_sounds.cpp +++ b/src/anm2/anm2_sounds.cpp @@ -1,7 +1,8 @@ #include "anm2.h" -#include "filesystem_.h" #include "map_.h" +#include "path_.h" +#include "working_directory.h" using namespace anm2ed::types; using namespace anm2ed::util; @@ -22,7 +23,7 @@ namespace anm2ed::anm2 labels.emplace_back(localize.get(BASIC_NONE)); for (auto& [id, sound] : content.sounds) { - auto pathString = filesystem::path_to_utf8(sound.path); + auto pathString = path::to_utf8(sound.path); labels.emplace_back(std::vformat(localize.get(FORMAT_SOUND), std::make_format_args(id, pathString))); } return labels; @@ -57,7 +58,7 @@ namespace anm2ed::anm2 return false; } - filesystem::WorkingDirectory workingDirectory(directory); + WorkingDirectory workingDirectory(directory); for (auto element = document.FirstChildElement("Sound"); element; element = element->NextSiblingElement("Sound")) { diff --git a/src/anm2/anm2_spritesheets.cpp b/src/anm2/anm2_spritesheets.cpp index 3d27cfa..284733a 100644 --- a/src/anm2/anm2_spritesheets.cpp +++ b/src/anm2/anm2_spritesheets.cpp @@ -2,8 +2,9 @@ #include -#include "filesystem_.h" #include "map_.h" +#include "path_.h" +#include "working_directory.h" using namespace anm2ed::types; using namespace anm2ed::util; @@ -41,7 +42,7 @@ namespace anm2ed::anm2 labels.emplace_back(localize.get(BASIC_NONE)); for (auto& [id, spritesheet] : content.spritesheets) { - auto pathString = filesystem::path_to_utf8(spritesheet.path); + auto pathString = path::to_utf8(spritesheet.path); labels.emplace_back(std::vformat(localize.get(FORMAT_SPRITESHEET), std::make_format_args(id, pathString))); } return labels; @@ -62,7 +63,7 @@ namespace anm2ed::anm2 return false; } - filesystem::WorkingDirectory workingDirectory(directory); + WorkingDirectory workingDirectory(directory); for (auto element = document.FirstChildElement("Spritesheet"); element; element = element->NextSiblingElement("Spritesheet")) diff --git a/src/anm2/frame.cpp b/src/anm2/frame.cpp index dde0cd6..1f4c82a 100644 --- a/src/anm2/frame.cpp +++ b/src/anm2/frame.cpp @@ -117,7 +117,5 @@ namespace anm2ed::anm2 } void Frame::shorten() { duration = glm::clamp(--duration, FRAME_DURATION_MIN, FRAME_DURATION_MAX); } - void Frame::extend() { duration = glm::clamp(++duration, FRAME_DURATION_MIN, FRAME_DURATION_MAX); } - } \ No newline at end of file diff --git a/src/anm2/frame.h b/src/anm2/frame.h index 9319113..9b2ee17 100644 --- a/src/anm2/frame.h +++ b/src/anm2/frame.h @@ -35,6 +35,8 @@ namespace anm2ed::anm2 MEMBERS #undef X +#undef MEMBERS + Frame() = default; Frame(tinyxml2::XMLElement*, Type); tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, Type); @@ -46,10 +48,29 @@ namespace anm2ed::anm2 struct FrameChange { -#define X(name, type, ...) std::optional name{}; - MEMBERS -#undef X + std::optional isVisible{}; + std::optional isInterpolated{}; + std::optional rotation{}; + std::optional duration{}; + std::optional atFrame{}; + std::optional eventID{}; + std::optional pivotX{}; + std::optional pivotY{}; + std::optional cropX{}; + std::optional cropY{}; + std::optional positionX{}; + std::optional positionY{}; + std::optional sizeX{}; + std::optional sizeY{}; + std::optional scaleX{}; + std::optional scaleY{}; + std::optional colorOffsetR{}; + std::optional colorOffsetG{}; + std::optional colorOffsetB{}; + std::optional tintR{}; + std::optional tintG{}; + std::optional tintB{}; + std::optional tintA{}; }; -#undef MEMBERS } diff --git a/src/anm2/item.cpp b/src/anm2/item.cpp index 5c66a35..17667e8 100644 --- a/src/anm2/item.cpp +++ b/src/anm2/item.cpp @@ -132,6 +132,39 @@ namespace anm2ed::anm2 auto end = numberFrames > -1 ? start + numberFrames : (int)frames.size(); end = glm::clamp(end, start, (int)frames.size()); + const auto clamp_identity = [](auto value) { return value; }; + const auto clamp01 = [](auto value) { return glm::clamp(value, 0.0f, 1.0f); }; + const auto clamp_duration = [](int value) { return std::max(FRAME_DURATION_MIN, value); }; + + auto apply_scalar_with_clamp = [&](auto& target, const auto& optionalValue, auto clampFunc) + { + if (!optionalValue) return; + auto value = *optionalValue; + + switch (type) + { + case ADJUST: + target = clampFunc(value); + break; + case ADD: + target = clampFunc(target + value); + break; + case SUBTRACT: + target = clampFunc(target - value); + break; + case MULTIPLY: + target = clampFunc(target * value); + break; + case DIVIDE: + if (value == decltype(value){}) return; + target = clampFunc(target / value); + break; + } + }; + + auto apply_scalar = [&](auto& target, const auto& optionalValue) + { apply_scalar_with_clamp(target, optionalValue, clamp_identity); }; + for (int i = useStart; i < end; i++) { Frame& frame = frames[i]; @@ -139,69 +172,32 @@ namespace anm2ed::anm2 if (change.isVisible) frame.isVisible = *change.isVisible; if (change.isInterpolated) frame.isInterpolated = *change.isInterpolated; - switch (type) - { - case ADJUST: - if (change.rotation) frame.rotation = *change.rotation; - if (change.duration) frame.duration = std::max(FRAME_DURATION_MIN, *change.duration); - if (change.crop) frame.crop = *change.crop; - if (change.pivot) frame.pivot = *change.pivot; - if (change.position) frame.position = *change.position; - if (change.size) frame.size = *change.size; - if (change.scale) frame.scale = *change.scale; - if (change.colorOffset) frame.colorOffset = glm::clamp(*change.colorOffset, 0.0f, 1.0f); - if (change.tint) frame.tint = glm::clamp(*change.tint, 0.0f, 1.0f); - break; + apply_scalar(frame.rotation, change.rotation); + apply_scalar_with_clamp(frame.duration, change.duration, clamp_duration); - case ADD: - if (change.rotation) frame.rotation += *change.rotation; - if (change.duration) frame.duration = std::max(FRAME_DURATION_MIN, frame.duration + *change.duration); - if (change.crop) frame.crop += *change.crop; - if (change.pivot) frame.pivot += *change.pivot; - if (change.position) frame.position += *change.position; - if (change.size) frame.size += *change.size; - if (change.scale) frame.scale += *change.scale; - if (change.colorOffset) frame.colorOffset = glm::clamp(frame.colorOffset + *change.colorOffset, 0.0f, 1.0f); - if (change.tint) frame.tint = glm::clamp(frame.tint + *change.tint, 0.0f, 1.0f); - break; + apply_scalar(frame.crop.x, change.cropX); + apply_scalar(frame.crop.y, change.cropY); - case SUBTRACT: - if (change.rotation) frame.rotation -= *change.rotation; - if (change.duration) frame.duration = std::max(FRAME_DURATION_MIN, frame.duration - *change.duration); - if (change.crop) frame.crop -= *change.crop; - if (change.pivot) frame.pivot -= *change.pivot; - if (change.position) frame.position -= *change.position; - if (change.size) frame.size -= *change.size; - if (change.scale) frame.scale -= *change.scale; - if (change.colorOffset) frame.colorOffset = glm::clamp(frame.colorOffset - *change.colorOffset, 0.0f, 1.0f); - if (change.tint) frame.tint = glm::clamp(frame.tint - *change.tint, 0.0f, 1.0f); - break; + apply_scalar(frame.pivot.x, change.pivotX); + apply_scalar(frame.pivot.y, change.pivotY); - case MULTIPLY: - if (change.rotation) frame.rotation *= *change.rotation; - if (change.duration) frame.duration = std::max(FRAME_DURATION_MIN, frame.duration * *change.duration); - if (change.crop) frame.crop *= *change.crop; - if (change.pivot) frame.pivot *= *change.pivot; - if (change.position) frame.position *= *change.position; - if (change.size) frame.size *= *change.size; - if (change.scale) frame.scale *= *change.scale; - if (change.colorOffset) frame.colorOffset = glm::clamp(frame.colorOffset * *change.colorOffset, 0.0f, 1.0f); - if (change.tint) frame.tint = glm::clamp(frame.tint * *change.tint, 0.0f, 1.0f); - break; + apply_scalar(frame.position.x, change.positionX); + apply_scalar(frame.position.y, change.positionY); - case DIVIDE: - if (change.rotation && *change.rotation != 0.0f) frame.rotation /= *change.rotation; - if (change.duration && *change.duration != 0) - frame.duration = std::max(FRAME_DURATION_MIN, frame.duration / *change.duration); - if (change.crop) frame.crop /= *change.crop; - if (change.pivot) frame.pivot /= *change.pivot; - if (change.position) frame.position /= *change.position; - if (change.size) frame.size /= *change.size; - if (change.scale) frame.scale /= *change.scale; - if (change.colorOffset) frame.colorOffset = glm::clamp(frame.colorOffset / *change.colorOffset, 0.0f, 1.0f); - if (change.tint) frame.tint = glm::clamp(frame.tint / *change.tint, 0.0f, 1.0f); - break; - } + apply_scalar(frame.size.x, change.sizeX); + apply_scalar(frame.size.y, change.sizeY); + + apply_scalar(frame.scale.x, change.scaleX); + apply_scalar(frame.scale.y, change.scaleY); + + apply_scalar_with_clamp(frame.colorOffset.x, change.colorOffsetR, clamp01); + apply_scalar_with_clamp(frame.colorOffset.y, change.colorOffsetG, clamp01); + apply_scalar_with_clamp(frame.colorOffset.z, change.colorOffsetB, clamp01); + + apply_scalar_with_clamp(frame.tint.x, change.tintR, clamp01); + apply_scalar_with_clamp(frame.tint.y, change.tintG, clamp01); + apply_scalar_with_clamp(frame.tint.z, change.tintB, clamp01); + apply_scalar_with_clamp(frame.tint.w, change.tintA, clamp01); } } diff --git a/src/anm2/sound.cpp b/src/anm2/sound.cpp index 04a61b8..95c3323 100644 --- a/src/anm2/sound.cpp +++ b/src/anm2/sound.cpp @@ -1,6 +1,7 @@ #include "sound.h" -#include "filesystem_.h" +#include "path_.h" +#include "working_directory.h" #include "xml_.h" using namespace anm2ed::resource; @@ -21,23 +22,11 @@ namespace anm2ed::anm2 return *this; } - namespace - { - std::filesystem::path make_relative_or_keep(const std::filesystem::path& input) - { - if (input.empty()) return input; - std::error_code ec{}; - auto relative = std::filesystem::relative(input, ec); - if (!ec) return relative; - return input; - } - } - Sound::Sound(const std::filesystem::path& directory, const std::filesystem::path& path) { - filesystem::WorkingDirectory workingDirectory(directory); - this->path = !path.empty() ? make_relative_or_keep(path) : this->path; - this->path = filesystem::path_lower_case_backslash_handle(this->path); + WorkingDirectory workingDirectory(directory); + this->path = !path.empty() ? path::make_relative(path) : this->path; + this->path = path::lower_case_backslash_handle(this->path); audio = Audio(this->path); } @@ -46,7 +35,7 @@ namespace anm2ed::anm2 if (!element) return; element->QueryIntAttribute("Id", &id); xml::query_path_attribute(element, "Path", &path); - path = filesystem::path_lower_case_backslash_handle(path); + path = path::lower_case_backslash_handle(path); audio = Audio(path); } @@ -54,7 +43,7 @@ namespace anm2ed::anm2 { auto element = document.NewElement("Sound"); element->SetAttribute("Id", id); - auto pathString = filesystem::path_to_utf8(path); + auto pathString = path::to_utf8(path); element->SetAttribute("Path", pathString.c_str()); return element; } @@ -72,8 +61,6 @@ namespace anm2ed::anm2 } void Sound::reload(const std::filesystem::path& directory) { *this = Sound(directory, this->path); } - bool Sound::is_valid() { return audio.is_valid(); } - void Sound::play() { audio.play(); } } diff --git a/src/anm2/spritesheet.cpp b/src/anm2/spritesheet.cpp index accd8a4..aff00ae 100644 --- a/src/anm2/spritesheet.cpp +++ b/src/anm2/spritesheet.cpp @@ -1,6 +1,7 @@ #include "spritesheet.h" -#include "filesystem_.h" +#include "path_.h" +#include "working_directory.h" #include "xml_.h" using namespace anm2ed::resource; @@ -17,27 +18,15 @@ namespace anm2ed::anm2 // Spritesheet paths from Isaac Rebirth are made with the assumption that paths are case-insensitive // However when using the resource dumper, the spritesheet paths are all lowercase (on Linux anyway) // This will handle this case and make the paths OS-agnostic - path = filesystem::path_lower_case_backslash_handle(path); + path = path::lower_case_backslash_handle(path); texture = Texture(path); } - namespace - { - std::filesystem::path make_relative_or_keep(const std::filesystem::path& input) - { - if (input.empty()) return input; - std::error_code ec{}; - auto relative = std::filesystem::relative(input, ec); - if (!ec) return relative; - return input; - } - } - Spritesheet::Spritesheet(const std::filesystem::path& directory, const std::filesystem::path& path) { - filesystem::WorkingDirectory workingDirectory(directory); - this->path = !path.empty() ? make_relative_or_keep(path) : this->path; - this->path = filesystem::path_lower_case_backslash_handle(this->path); + WorkingDirectory workingDirectory(directory); + this->path = !path.empty() ? path::make_relative(path) : this->path; + this->path = path::lower_case_backslash_handle(this->path); texture = Texture(this->path); } @@ -45,7 +34,7 @@ namespace anm2ed::anm2 { auto element = document.NewElement("Spritesheet"); element->SetAttribute("Id", id); - auto pathString = filesystem::path_to_utf8(path); + auto pathString = path::to_utf8(path); element->SetAttribute("Path", pathString.c_str()); return element; } @@ -64,8 +53,8 @@ namespace anm2ed::anm2 bool Spritesheet::save(const std::filesystem::path& directory, const std::filesystem::path& path) { - filesystem::WorkingDirectory workingDirectory(directory); - this->path = !path.empty() ? make_relative_or_keep(path) : this->path; + WorkingDirectory workingDirectory(directory); + this->path = !path.empty() ? path::make_relative(path) : this->path; return texture.write_png(this->path); } diff --git a/src/canvas.cpp b/src/canvas.cpp index 3ad14c9..23aa8c0 100644 --- a/src/canvas.cpp +++ b/src/canvas.cpp @@ -11,15 +11,9 @@ using namespace glm; using namespace anm2ed::resource; using namespace anm2ed::util; -using namespace anm2ed::canvas; namespace anm2ed { - constexpr float AXIS_VERTICES[] = {-1.0f, 0.0f, 1.0f, 0.0f}; - constexpr float GRID_VERTICES[] = {-1.f, -1.f, 0.f, 0.f, 3.f, -1.f, 2.f, 0.f, -1.f, 3.f, 0.f, 2.f}; - constexpr float RECT_VERTICES[] = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f}; - constexpr GLuint TEXTURE_INDICES[] = {0, 1, 2, 2, 3, 0}; - Canvas::Canvas() = default; Canvas::Canvas(vec2 size) diff --git a/src/canvas.h b/src/canvas.h index f4625a2..85cddcd 100644 --- a/src/canvas.h +++ b/src/canvas.h @@ -6,35 +6,37 @@ #include "framebuffer.h" #include "shader.h" -namespace anm2ed::canvas -{ - constexpr float TEXTURE_VERTICES[] = {0, 0, 0.0f, 0.0f, 1, 0, 1.0f, 0.0f, 1, 1, 1.0f, 1.0f, 0, 1, 0.0f, 1.0f}; - - constexpr auto PIVOT_SIZE = glm::vec2(8, 8); - constexpr auto ZOOM_MIN = 1.0f; - constexpr auto ZOOM_MAX = 2000.0f; - - constexpr auto DASH_LENGTH = 4.0f; - constexpr auto DASH_GAP = 1.0f; - constexpr auto DASH_OFFSET = 1.0f; - - constexpr auto STEP = 1.0f; - constexpr auto STEP_FAST = 5.0f; - - constexpr auto GRID_SIZE_MIN = 1; - constexpr auto GRID_SIZE_MAX = 10000; - constexpr auto GRID_OFFSET_MIN = 0; - constexpr auto GRID_OFFSET_MAX = 10000; - - constexpr auto CHECKER_SIZE = 32.0f; -} +#include "types.h" namespace anm2ed { - class Canvas : public Framebuffer { public: + static constexpr float TEXTURE_VERTICES[] = {0, 0, 0.0f, 0.0f, 1, 0, 1.0f, 0.0f, + 1, 1, 1.0f, 1.0f, 0, 1, 0.0f, 1.0f}; + static constexpr auto PIVOT_SIZE = glm::vec2(8, 8); + static constexpr auto ZOOM_MIN = 1.0f; + static constexpr auto ZOOM_MAX = 2000.0f; + static constexpr auto DASH_LENGTH = 4.0f; + static constexpr auto DASH_GAP = 1.0f; + static constexpr auto DASH_OFFSET = 1.0f; + static constexpr auto STEP = 1.0f; + static constexpr auto STEP_FAST = 5.0f; + static constexpr auto GRID_SIZE_MIN = 1; + static constexpr auto GRID_SIZE_MAX = 10000; + static constexpr auto GRID_OFFSET_MIN = 0; + static constexpr auto GRID_OFFSET_MAX = 10000; + static constexpr auto CHECKER_SIZE = 32.0f; + static constexpr float AXIS_VERTICES[] = {-1.0f, 0.0f, 1.0f, 0.0f}; + static constexpr float GRID_VERTICES[] = {-1.f, -1.f, 0.f, 0.f, 3.f, -1.f, 2.f, 0.f, -1.f, 3.f, 0.f, 2.f}; + static constexpr float RECT_VERTICES[] = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f}; + static constexpr GLuint TEXTURE_INDICES[] = {0, 1, 2, 2, 3, 0}; + static constexpr auto BORDER_DASH_LENGTH = 1.0f; + static constexpr auto BORDER_DASH_GAP = 0.5f; + static constexpr auto BORDER_DASH_OFFSET = 0.0f; + static constexpr auto PIVOT_COLOR = types::color::PINK; + GLuint axisVAO{}; GLuint axisVBO{}; GLuint rectVAO{}; @@ -53,10 +55,9 @@ namespace anm2ed void grid_render(resource::Shader&, float, glm::vec2, glm::ivec2 = glm::ivec2(32, 32), glm::ivec2 = {}, glm::vec4 = glm::vec4(1.0f)) const; void texture_render(resource::Shader&, GLuint&, glm::mat4 = {1.0f}, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {}, - float* = (float*)canvas::TEXTURE_VERTICES) const; + float* = (float*)TEXTURE_VERTICES) const; void rect_render(resource::Shader&, const glm::mat4&, const glm::mat4&, glm::vec4 = glm::vec4(1.0f), - float dashLength = canvas::DASH_LENGTH, float dashGap = canvas::DASH_GAP, - float dashOffset = canvas::DASH_OFFSET) const; + float dashLength = DASH_LENGTH, float dashGap = DASH_GAP, float dashOffset = DASH_OFFSET) const; void zoom_set(float&, glm::vec2&, glm::vec2, float) const; glm::vec2 position_translate(float&, glm::vec2&, glm::vec2) const; void set_to_rect(float& zoom, glm::vec2& pan, glm::vec4 rect) const; diff --git a/src/dialog.cpp b/src/dialog.cpp index 8c3d5d0..32b4efb 100644 --- a/src/dialog.cpp +++ b/src/dialog.cpp @@ -13,9 +13,11 @@ #include #include -#include "filesystem_.h" +#include "path_.h" -namespace anm2ed::dialog +using namespace anm2ed::util; + +namespace anm2ed { static void callback(void* userData, const char* const* filelist, int filter) { @@ -23,7 +25,7 @@ namespace anm2ed::dialog if (filelist && filelist[0] && strlen(filelist[0]) > 0) { - self->path = anm2ed::util::filesystem::path_from_utf8(filelist[0]); + self->path = path::from_utf8(filelist[0]); self->selectedFilter = filter; } else @@ -32,13 +34,6 @@ namespace anm2ed::dialog self->path.clear(); } } -} - -using namespace anm2ed::dialog; -namespace filesystem = anm2ed::util::filesystem; - -namespace anm2ed -{ Dialog::Dialog(SDL_Window* window) { @@ -46,21 +41,21 @@ namespace anm2ed this->window = window; } - void Dialog::file_open(dialog::Type type) + void Dialog::file_open(Type type) { SDL_ShowOpenFileDialog(callback, this, window, FILTERS[TYPE_FILTERS[type]], std::size(FILTERS[TYPE_FILTERS[type]]), nullptr, false); this->type = type; } - void Dialog::file_save(dialog::Type type) + void Dialog::file_save(Type type) { SDL_ShowSaveFileDialog(callback, this, window, FILTERS[TYPE_FILTERS[type]], std::size(FILTERS[TYPE_FILTERS[type]]), nullptr); this->type = type; } - void Dialog::folder_open(dialog::Type type) + void Dialog::folder_open(Type type) { SDL_ShowOpenFolderDialog(callback, this, window, nullptr, false); this->type = type; @@ -72,7 +67,7 @@ namespace anm2ed #ifdef _WIN32 ShellExecuteW(nullptr, L"open", path.native().c_str(), nullptr, nullptr, SW_SHOWNORMAL); #elif __unix__ - auto pathUtf8 = filesystem::path_to_utf8(path); + auto pathUtf8 = path::to_utf8(path); system(std::format("xdg-open \"{}\" &", pathUtf8).c_str()); #else toasts.push(localize.get(TOAST_NOT_SUPPORTED)); @@ -82,6 +77,6 @@ namespace anm2ed void Dialog::reset() { *this = Dialog(this->window); } - bool Dialog::is_selected(dialog::Type type) const { return this->type == type && !path.empty(); } + bool Dialog::is_selected(Type type) const { return this->type == type && !path.empty(); } }; diff --git a/src/dialog.h b/src/dialog.h index bcb1fc4..d48a7bf 100644 --- a/src/dialog.h +++ b/src/dialog.h @@ -4,8 +4,11 @@ #include -namespace anm2ed::dialog +namespace anm2ed { + class Dialog + { + public: #if defined(_WIN32) #define EXECUTABLE_FILTER {"Executable", "exe"} #else @@ -22,18 +25,18 @@ namespace anm2ed::dialog X(MP4, {"MP4 video", "MP4"}) \ X(EXECUTABLE, EXECUTABLE_FILTER) - enum Filter - { + enum Filter + { #define X(symbol, ...) symbol, - FILTER_LIST -#undef X - }; - - constexpr SDL_DialogFileFilter FILTERS[][1] = { -#define X(symbol, ...) {__VA_ARGS__}, FILTER_LIST #undef X - }; + }; + + static constexpr SDL_DialogFileFilter FILTERS[][1] = { +#define X(symbol, ...) {__VA_ARGS__}, + FILTER_LIST +#undef X + }; #undef FILTER_LIST @@ -53,40 +56,33 @@ namespace anm2ed::dialog X(WEBM_PATH_SET, WEBM) \ X(MP4_PATH_SET, MP4) - enum Type - { + enum Type + { #define X(symbol, filter) symbol, - DIALOG_LIST -#undef X - }; - - constexpr Filter TYPE_FILTERS[] = { -#define X(symbol, filter) filter, DIALOG_LIST #undef X - }; + }; + + static constexpr Filter TYPE_FILTERS[] = { +#define X(symbol, filter) filter, + DIALOG_LIST +#undef X + }; #undef DIALOG_LIST -} -namespace anm2ed -{ - - class Dialog - { - public: SDL_Window* window{}; std::filesystem::path path{}; - dialog::Type type{dialog::NONE}; + Type type{NONE}; int selectedFilter{-1}; Dialog() = default; Dialog(SDL_Window*); - void file_open(dialog::Type type); - void file_save(dialog::Type type); - void folder_open(dialog::Type type); - bool is_selected(dialog::Type type) const; + void file_open(Type type); + void file_save(Type type); + void folder_open(Type type); + bool is_selected(Type type) const; void reset(); void file_explorer_open(const std::filesystem::path&); }; -} +} \ No newline at end of file diff --git a/src/document.cpp b/src/document.cpp index 0443b49..a049840 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -4,18 +4,17 @@ #include -#include "filesystem_.h" #include "log.h" +#include "path_.h" #include "strings.h" #include "toast.h" using namespace anm2ed::anm2; using namespace anm2ed::imgui; using namespace anm2ed::types; +using namespace anm2ed::util; using namespace glm; -namespace filesystem = anm2ed::util::filesystem; - namespace anm2ed { Document::Document(Anm2& anm2, const std::filesystem::path& path) @@ -99,12 +98,12 @@ namespace anm2ed this->path = !path.empty() ? path : this->path; auto absolutePath = this->path; - auto absolutePathUtf8 = filesystem::path_to_utf8(absolutePath); + auto absolutePathUtf8 = path::to_utf8(absolutePath); if (anm2.serialize(absolutePath, errorString)) { toasts.push(std::vformat(localize.get(TOAST_SAVE_DOCUMENT), std::make_format_args(absolutePathUtf8))); - logger.info(std::vformat(localize.get(TOAST_SAVE_DOCUMENT, anm2ed::ENGLISH), - std::make_format_args(absolutePathUtf8))); + logger.info( + std::vformat(localize.get(TOAST_SAVE_DOCUMENT, anm2ed::ENGLISH), std::make_format_args(absolutePathUtf8))); clean(); return true; } @@ -121,14 +120,14 @@ namespace anm2ed std::filesystem::path Document::autosave_path_get() { - auto fileNameUtf8 = filesystem::path_to_utf8(filename_get()); + auto fileNameUtf8 = path::to_utf8(filename_get()); auto autosaveNameUtf8 = "." + fileNameUtf8 + ".autosave"; - return directory_get() / filesystem::path_from_utf8(autosaveNameUtf8); + return directory_get() / path::from_utf8(autosaveNameUtf8); } std::filesystem::path Document::path_from_autosave_get(std::filesystem::path& path) { - auto fileName = path.filename().u8string(); + auto fileName = path::to_utf8(path.filename()); if (!fileName.empty() && fileName.front() == '.') fileName.erase(fileName.begin()); auto restorePath = path.parent_path() / std::filesystem::path(std::u8string(fileName.begin(), fileName.end())); @@ -140,7 +139,7 @@ namespace anm2ed bool Document::autosave(std::string* errorString) { auto autosavePath = autosave_path_get(); - auto autosavePathUtf8 = filesystem::path_to_utf8(autosavePath); + auto autosavePathUtf8 = path::to_utf8(autosavePath); if (anm2.serialize(autosavePath, errorString)) { autosaveHash = hash; @@ -152,8 +151,8 @@ namespace anm2ed } else if (errorString) { - toasts.push(std::vformat(localize.get(TOAST_AUTOSAVE_FAILED), - std::make_format_args(autosavePathUtf8, *errorString))); + toasts.push( + std::vformat(localize.get(TOAST_AUTOSAVE_FAILED), std::make_format_args(autosavePathUtf8, *errorString))); logger.error(std::vformat(localize.get(TOAST_AUTOSAVE_FAILED, anm2ed::ENGLISH), std::make_format_args(autosavePathUtf8, *errorString))); } @@ -265,17 +264,16 @@ namespace anm2ed if (anm2.spritesheet_add(directory_get(), path, id)) { anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; - auto pathString = filesystem::path_to_utf8(spritesheet.path); + auto pathString = path::to_utf8(spritesheet.path); this->spritesheet.selection = {id}; this->spritesheet.reference = id; - toasts.push( - std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED), std::make_format_args(id, pathString))); + toasts.push(std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED), std::make_format_args(id, pathString))); logger.info(std::vformat(localize.get(TOAST_SPRITESHEET_INITIALIZED, anm2ed::ENGLISH), std::make_format_args(id, pathString))); } else { - auto pathUtf8 = filesystem::path_to_utf8(pathCopy); + auto pathUtf8 = path::to_utf8(pathCopy); toasts.push(std::vformat(localize.get(TOAST_SPRITESHEET_INIT_FAILED), std::make_format_args(pathUtf8))); logger.error(std::vformat(localize.get(TOAST_SPRITESHEET_INIT_FAILED, anm2ed::ENGLISH), std::make_format_args(pathUtf8))); @@ -294,7 +292,7 @@ namespace anm2ed if (anm2.sound_add(directory_get(), path, id)) { auto& soundInfo = anm2.content.sounds[id]; - auto soundPath = filesystem::path_to_utf8(soundInfo.path); + auto soundPath = path::to_utf8(soundInfo.path); sound.selection = {id}; sound.reference = id; toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZED), std::make_format_args(id, soundPath))); @@ -303,7 +301,7 @@ namespace anm2ed } else { - auto pathUtf8 = filesystem::path_to_utf8(pathCopy); + auto pathUtf8 = path::to_utf8(pathCopy); toasts.push(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED), std::make_format_args(pathUtf8))); logger.error(std::vformat(localize.get(TOAST_SOUND_INITIALIZE_FAILED, anm2ed::ENGLISH), std::make_format_args(pathUtf8))); diff --git a/src/imgui/documents.cpp b/src/imgui/documents.cpp index def84e8..529513c 100644 --- a/src/imgui/documents.cpp +++ b/src/imgui/documents.cpp @@ -3,14 +3,13 @@ #include #include -#include "filesystem_.h" +#include "path_.h" #include "strings.h" #include "time_.h" using namespace anm2ed::resource; using namespace anm2ed::types; using namespace anm2ed::util; -namespace filesystem = anm2ed::util::filesystem; namespace anm2ed::imgui { @@ -88,7 +87,7 @@ namespace anm2ed::imgui auto isRequested = i == manager.pendingSelected; auto font = isDirty ? font::ITALICS : font::REGULAR; - auto filename = filesystem::path_to_utf8(document.filename_get()); + auto filename = path::to_utf8(document.filename_get()); auto string = isDirty ? std::vformat(localize.get(FORMAT_NOT_SAVED), std::make_format_args(filename)) : filename; auto label = std::format("{}###Document{}", string, i); @@ -105,7 +104,7 @@ namespace anm2ed::imgui ImGui::EndTabItem(); } - auto pathUtf8 = filesystem::path_to_utf8(document.path); + auto pathUtf8 = path::to_utf8(document.path); ImGui::SetItemTooltip("%s", pathUtf8.c_str()); ImGui::PopFont(); @@ -128,7 +127,7 @@ namespace anm2ed::imgui { auto& closeDocument = manager.documents[closeDocumentIndex]; - auto filename = filesystem::path_to_utf8(closeDocument.filename_get()); + auto filename = path::to_utf8(closeDocument.filename_get()); auto prompt = std::vformat(localize.get(LABEL_DOCUMENT_MODIFIED_PROMPT), std::make_format_args(filename)); ImGui::TextUnformatted(prompt.c_str()); diff --git a/src/imgui/imgui_.cpp b/src/imgui/imgui_.cpp index efc5c4d..41aa3f8 100644 --- a/src/imgui/imgui_.cpp +++ b/src/imgui/imgui_.cpp @@ -1,6 +1,3 @@ -#include "imgui_.h" -#include "strings.h" - #include #include @@ -8,7 +5,12 @@ #include #include +#include "imgui_.h" +#include "path_.h" +#include "strings.h" + using namespace anm2ed::types; +using namespace anm2ed::util; using namespace glm; namespace anm2ed::imgui @@ -84,6 +86,17 @@ namespace anm2ed::imgui return ImGui::InputText(label, string->data(), string->capacity() + 1, flags, input_text_callback, string); } + bool input_text_path(const char* label, std::filesystem::path* path, ImGuiInputTextFlags flags) + { + if (!path) return false; + + auto pathUtf8 = path::to_utf8(*path); + auto edited = input_text_string(label, &pathUtf8, flags); + if (edited) *path = path::from_utf8(pathUtf8); + + return edited; + } + bool combo_negative_one_indexed(const std::string& label, int* index, std::vector& strings) { *index += 1; diff --git a/src/imgui/imgui_.h b/src/imgui/imgui_.h index 25bda14..99697dd 100644 --- a/src/imgui/imgui_.h +++ b/src/imgui/imgui_.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -176,6 +177,7 @@ namespace anm2ed::imgui ImVec2 child_size_get(int = 1); int input_text_callback(ImGuiInputTextCallbackData*); bool input_text_string(const char*, std::string*, ImGuiInputTextFlags = 0); + bool input_text_path(const char*, std::filesystem::path*, ImGuiInputTextFlags = 0); bool input_int_range(const char*, int&, int, int, int = STEP, int = STEP_FAST, ImGuiInputTextFlags = 0); bool input_int2_range(const char*, glm::ivec2&, glm::ivec2, glm::ivec2, ImGuiInputTextFlags = 0); bool input_float_range(const char*, float&, float, float, float = STEP, float = STEP_FAST, const char* = "%.3f", diff --git a/src/imgui/taskbar.cpp b/src/imgui/taskbar.cpp index 4462878..b996078 100644 --- a/src/imgui/taskbar.cpp +++ b/src/imgui/taskbar.cpp @@ -1,228 +1,21 @@ #include "taskbar.h" -#include -#include -#include -#include -#include #include #include #include -#include -#include #include #include -#include "filesystem_.h" -#include "log.h" -#include "math_.h" -#include "render.h" -#include "shader.h" -#include "snapshots.h" -#include "types.h" - -#include "icon.h" -#include "toast.h" - #include "strings.h" +#include "types.h" using namespace anm2ed::resource; using namespace anm2ed::types; -using namespace anm2ed::canvas; -using namespace anm2ed::util; using namespace glm; -namespace filesystem = anm2ed::util::filesystem; namespace anm2ed::imgui { - namespace - { -#ifndef _WIN32 - constexpr auto EXEC_PERMS = - std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec; -#endif - - bool ffmpeg_is_executable(const std::string& pathString) - { - if (pathString.empty()) return false; - - std::error_code ec{}; - auto status = std::filesystem::status(pathString, ec); - if (ec || !std::filesystem::is_regular_file(status)) return false; - -#ifndef _WIN32 - if ((status.permissions() & EXEC_PERMS) == std::filesystem::perms::none) return false; -#endif - return true; - } - - bool png_directory_ensure(const std::string& directory) - { - if (directory.empty()) - { - toasts.push(localize.get(TOAST_PNG_DIRECTORY_NOT_SET)); - logger.warning(localize.get(TOAST_PNG_DIRECTORY_NOT_SET, anm2ed::ENGLISH)); - return false; - } - - std::error_code ec{}; - auto pathValue = std::filesystem::path(directory); - auto exists = std::filesystem::exists(pathValue, ec); - - if (ec) - { - auto errorMessage = ec.message(); - toasts.push(std::vformat(localize.get(TOAST_PNG_DIRECTORY_ACCESS_ERROR), - std::make_format_args(directory, errorMessage))); - logger.error(std::vformat(localize.get(TOAST_PNG_DIRECTORY_ACCESS_ERROR, anm2ed::ENGLISH), - std::make_format_args(directory, errorMessage))); - return false; - } - - if (exists) - { - if (!std::filesystem::is_directory(pathValue, ec) || ec) - { - toasts.push(std::vformat(localize.get(TOAST_PNG_DIRECTORY_NOT_DIRECTORY), std::make_format_args(directory))); - logger.warning(std::vformat(localize.get(TOAST_PNG_DIRECTORY_NOT_DIRECTORY, anm2ed::ENGLISH), - std::make_format_args(directory))); - return false; - } - return true; - } - - if (!std::filesystem::create_directories(pathValue, ec) || ec) - { - auto errorMessage = ec.message(); - toasts.push(std::vformat(localize.get(TOAST_PNG_DIRECTORY_CREATE_ERROR), - std::make_format_args(directory, errorMessage))); - logger.error(std::vformat(localize.get(TOAST_PNG_DIRECTORY_CREATE_ERROR, anm2ed::ENGLISH), - std::make_format_args(directory, errorMessage))); - return false; - } - - return true; - } - } - - static constexpr auto CREDIT_DELAY = 1.0f; - static constexpr auto CREDIT_SCROLL_SPEED = 25.0f; - - struct Credit - { - const char* string{}; - font::Type font{font::REGULAR}; - }; - - struct ScrollingCredit - { - int index{}; - float offset{}; - }; - - struct CreditsState - { - std::vector active{}; - float spawnTimer{1.0f}; - int nextIndex{}; - }; - - static constexpr Credit CREDITS[] = { - {"Anm2Ed", font::BOLD}, - {"License: GPLv3"}, - {""}, - {"Designer", font::BOLD}, - {"Shweet"}, - {""}, - {"Additional Help", font::BOLD}, - {"im-tem"}, - {""}, - {"Localization", font::BOLD}, - {"Gabriel Asencio (Spanish (Latin America))"}, - {"ExtremeThreat (Russian)"}, - {"CxRedix (Chinese)"}, - {"sawalk/사왈이 (Korean)"}, - {""}, - {"Based on the work of:", font::BOLD}, - {"Adrian Gavrilita"}, - {"Simon Parzer"}, - {"Matt Kapuszczak"}, - {""}, - {"XM Music", font::BOLD}, - {"Drozerix"}, - {"\"Keygen Wraith\""}, - {"https://modarchive.org/module.php?207854"}, - {"License: CC0"}, - {""}, - {"Libraries", font::BOLD}, - {"Dear ImGui"}, - {"https://github.com/ocornut/imgui"}, - {"License: MIT"}, - {""}, - {"SDL"}, - {"https://github.com/libsdl-org/SDL"}, - {"License: zlib"}, - {""}, - {"SDL_mixer"}, - {"https://github.com/libsdl-org/SDL_mixer"}, - {"License: zlib"}, - {""}, - {"tinyxml2"}, - {"https://github.com/leethomason/tinyxml2"}, - {"License: zlib"}, - {""}, - {"glm"}, - {"https://github.com/g-truc/glm"}, - {"License: MIT"}, - {""}, - {"lunasvg"}, - {"https://github.com/sammycage/lunasvg"}, - {"License: MIT"}, - {""}, - {"Icons", font::BOLD}, - {"Remix Icons"}, - {"remixicon.com"}, - {"License: Apache"}, - {""}, - {"Font", font::BOLD}, - {"Noto Sans"}, - {"https://fonts.google.com/noto/specimen/Noto+Sans"}, - {"License: OFL"}, - {""}, - {"Special Thanks", font::BOLD}, - {"Edmund McMillen"}, - {"Florian Himsl"}, - {"Tyrone Rodriguez"}, - {"The-Vinh Truong (_kilburn)"}, - {"Isaac Reflashed team"}, - {"Everyone who waited patiently for this to be finished"}, - {"Everyone else who has worked on The Binding of Isaac!"}, - {""}, - {""}, - {""}, - {""}, - {""}, - {""}, - {""}, - {""}, - {""}, - {"enjoy the jams :)"}, - {""}, - {""}, - {""}, - {""}, - {""}, - {""}, - {""}, - {""}, - {""}, - {""}, - }; - static constexpr auto CREDIT_COUNT = (int)(sizeof(CREDITS) / sizeof(Credit)); - - Taskbar::Taskbar() : generate(vec2()) {} - void Taskbar::update(Manager& manager, Settings& settings, Resources& resources, Dialog& dialog, bool& isQuitting) { auto document = manager.get(); @@ -236,19 +29,17 @@ namespace anm2ed::imgui if (ImGui::BeginMenu(localize.get(LABEL_FILE_MENU))) { - if (ImGui::MenuItem(localize.get(BASIC_NEW), settings.shortcutNew.c_str())) dialog.file_save(dialog::ANM2_NEW); + if (ImGui::MenuItem(localize.get(BASIC_NEW), settings.shortcutNew.c_str())) dialog.file_save(Dialog::ANM2_NEW); if (ImGui::MenuItem(localize.get(BASIC_OPEN), settings.shortcutOpen.c_str())) - dialog.file_open(dialog::ANM2_OPEN); + dialog.file_open(Dialog::ANM2_OPEN); auto recentFiles = manager.recent_files_ordered(); if (ImGui::BeginMenu(localize.get(LABEL_OPEN_RECENT), !recentFiles.empty())) { - for (std::size_t index = 0; index < recentFiles.size(); ++index) + for (auto [i, file] : std::views::enumerate(recentFiles)) { - const auto& file = recentFiles[index]; + ImGui::PushID((int)i); auto label = std::format(FILE_LABEL_FORMAT, file.filename().string(), file.string()); - - ImGui::PushID((int)index); if (ImGui::MenuItem(label.c_str())) manager.open(file.string()); ImGui::PopID(); } @@ -268,7 +59,7 @@ namespace anm2ed::imgui } if (ImGui::MenuItem(localize.get(LABEL_SAVE_AS), settings.shortcutSaveAs.c_str(), false, document)) - dialog.file_save(dialog::ANM2_SAVE); + dialog.file_save(Dialog::ANM2_SAVE); if (ImGui::MenuItem(localize.get(LABEL_EXPLORE_XML_LOCATION), nullptr, false, document)) dialog.file_explorer_open(document->directory_get()); @@ -276,19 +67,19 @@ namespace anm2ed::imgui if (ImGui::MenuItem(localize.get(LABEL_EXIT), settings.shortcutExit.c_str())) isQuitting = true; ImGui::EndMenu(); } - if (dialog.is_selected(dialog::ANM2_NEW)) + if (dialog.is_selected(Dialog::ANM2_NEW)) { manager.new_(dialog.path); dialog.reset(); } - if (dialog.is_selected(dialog::ANM2_OPEN)) + if (dialog.is_selected(Dialog::ANM2_OPEN)) { manager.open(dialog.path); dialog.reset(); } - if (dialog.is_selected(dialog::ANM2_SAVE)) + if (dialog.is_selected(Dialog::ANM2_SAVE)) { manager.save(dialog.path); dialog.reset(); @@ -306,7 +97,8 @@ namespace anm2ed::imgui ImGui::Separator(); - if (ImGui::MenuItem(localize.get(LABEL_TASKBAR_RENDER_ANIMATION), nullptr, false, animation)) + if (ImGui::MenuItem(localize.get(LABEL_TASKBAR_RENDER_ANIMATION), nullptr, false, + animation && manager.isAbleToRecord)) renderPopup.open(); ImGui::EndMenu(); } @@ -324,10 +116,10 @@ namespace anm2ed::imgui if (ImGui::BeginMenu(localize.get(LABEL_WINDOW_MENU))) { - for (std::size_t index = 0; index < WINDOW_COUNT; ++index) + for (int i = 0; i < WINDOW_COUNT; i++) { - auto member = WINDOW_MEMBERS[index]; - ImGui::MenuItem(localize.get(::anm2ed::WINDOW_STRING_TYPES[index]), nullptr, &(settings.*member)); + auto member = WINDOW_MEMBERS[i]; + ImGui::MenuItem(localize.get(::anm2ed::WINDOW_STRING_TYPES[i]), nullptr, &(settings.*member)); } ImGui::EndMenu(); @@ -335,12 +127,7 @@ namespace anm2ed::imgui if (ImGui::BeginMenu(localize.get(LABEL_SETTINGS_MENU))) { - if (ImGui::MenuItem(localize.get(LABEL_TASKBAR_CONFIGURE))) - { - editSettings = settings; - configurePopup.open(); - } - + if (ImGui::MenuItem(localize.get(LABEL_TASKBAR_CONFIGURE))) configurePopup.open(); ImGui::EndMenu(); } @@ -357,99 +144,11 @@ namespace anm2ed::imgui if (ImGui::BeginPopupModal(generatePopup.label(), &generatePopup.isOpen, ImGuiWindowFlags_NoResize)) { - auto& startPosition = settings.generateStartPosition; - auto& size = settings.generateSize; - auto& pivot = settings.generatePivot; - auto& rows = settings.generateRows; - auto& columns = settings.generateColumns; - auto& count = settings.generateCount; - auto& delay = settings.generateDuration; - auto& zoom = settings.generateZoom; - auto& zoomStep = settings.inputZoomStep; - - auto childSize = ImVec2(row_widget_width_get(2), size_without_footer_get().y); - - if (ImGui::BeginChild("##Options Child", childSize, ImGuiChildFlags_Borders)) + if (document) { - ImGui::InputInt2(localize.get(LABEL_GENERATE_START_POSITION), value_ptr(startPosition)); - ImGui::InputInt2(localize.get(LABEL_GENERATE_FRAME_SIZE), value_ptr(size)); - ImGui::InputInt2(localize.get(BASIC_PIVOT), value_ptr(pivot)); - ImGui::InputInt(localize.get(LABEL_GENERATE_ROWS), &rows, STEP, STEP_FAST); - ImGui::InputInt(localize.get(LABEL_GENERATE_COLUMNS), &columns, STEP, STEP_FAST); - - input_int_range(localize.get(LABEL_GENERATE_COUNT), count, anm2::FRAME_NUM_MIN, rows * columns); - - ImGui::InputInt(localize.get(BASIC_DURATION), &delay, STEP, STEP_FAST); + generateAnimationFromGrid.update(*document, resources, settings); + if (generateAnimationFromGrid.isEnd) generatePopup.close(); } - ImGui::EndChild(); - - ImGui::SameLine(); - - if (ImGui::BeginChild("##Preview Child", childSize, ImGuiChildFlags_Borders)) - { - auto& backgroundColor = settings.previewBackgroundColor; - auto& time = generateTime; - auto& shaderTexture = resources.shaders[resource::shader::TEXTURE]; - - auto previewSize = ImVec2(ImGui::GetContentRegionAvail().x, size_without_footer_get(2).y); - - generate.size_set(to_vec2(previewSize)); - generate.bind(); - generate.viewport_set(); - generate.clear(vec4(backgroundColor, 1.0f)); - - if (document && document->reference.itemType == anm2::LAYER) - { - auto& texture = document->anm2.content - .spritesheets[document->anm2.content.layers[document->reference.itemID].spritesheetID] - .texture; - - auto index = std::clamp((int)(time * (count - 1)), 0, (count - 1)); - auto row = index / columns; - auto column = index % columns; - auto crop = startPosition + ivec2(size.x * column, size.y * row); - auto uvMin = (vec2(crop) + vec2(0.5f)) / vec2(texture.size); - auto uvMax = (vec2(crop) + vec2(size) - vec2(0.5f)) / vec2(texture.size); - - mat4 transform = generate.transform_get(zoom) * math::quad_model_get(size, {}, pivot); - - generate.texture_render(shaderTexture, texture.id, transform, vec4(1.0f), {}, - math::uv_vertices_get(uvMin, uvMax).data()); - } - generate.unbind(); - - ImGui::Image(generate.texture, previewSize); - - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - ImGui::SliderFloat("##Time", &time, 0.0f, 1.0f, ""); - - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - ImGui::InputFloat("##Zoom", &zoom, zoomStep, zoomStep, "%.0f%%"); - zoom = glm::clamp(zoom, ZOOM_MIN, ZOOM_MAX); - } - - ImGui::EndChild(); - - auto widgetSize = widget_size_with_row_get(2); - - if (ImGui::Button(localize.get(LABEL_GENERATE), widgetSize)) - { - auto generate_from_grid = [&]() - { - item->frames_generate_from_grid(startPosition, size, pivot, columns, count, delay); - animation->frameNum = animation->length(); - }; - - DOCUMENT_EDIT_PTR(document, localize.get(EDIT_GENERATE_ANIMATION_FROM_GRID), Document::FRAMES, - generate_from_grid()); - - generatePopup.close(); - } - - ImGui::SameLine(); - - if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) generatePopup.close(); - ImGui::EndPopup(); } @@ -457,641 +156,42 @@ namespace anm2ed::imgui if (ImGui::BeginPopupModal(changePopup.label(), &changePopup.isOpen, ImGuiWindowFlags_NoResize)) { - auto& isCrop = settings.changeIsCrop; - auto& isSize = settings.changeIsSize; - auto& isPosition = settings.changeIsPosition; - auto& isPivot = settings.changeIsPivot; - auto& isScale = settings.changeIsScale; - auto& isRotation = settings.changeIsRotation; - auto& isDuration = settings.changeIsDuration; - auto& isTint = settings.changeIsTint; - auto& isColorOffset = settings.changeIsColorOffset; - auto& isVisibleSet = settings.changeIsVisibleSet; - auto& isInterpolatedSet = settings.changeIsInterpolatedSet; - auto& crop = settings.changeCrop; - auto& size = settings.changeSize; - auto& position = settings.changePosition; - auto& pivot = settings.changePivot; - auto& scale = settings.changeScale; - auto& rotation = settings.changeRotation; - auto& duration = settings.changeDuration; - auto& tint = settings.changeTint; - auto& colorOffset = settings.changeColorOffset; - auto& isVisible = settings.changeIsVisible; - auto& isInterpolated = settings.changeIsInterpolated; - -#define PROPERTIES_WIDGET(body) \ - ImGui::Checkbox(checkboxLabel, &isEnabled); \ - ImGui::SameLine(); \ - ImGui::BeginDisabled(!isEnabled); \ - body; \ - ImGui::EndDisabled(); - - auto bool_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, bool& value) - { PROPERTIES_WIDGET(ImGui::Checkbox(valueLabel, &value)); }; - - auto color3_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec3& value) - { PROPERTIES_WIDGET(ImGui::ColorEdit3(valueLabel, value_ptr(value))); }; - - auto color4_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec4& value) - { PROPERTIES_WIDGET(ImGui::ColorEdit4(valueLabel, value_ptr(value))); }; - - auto float2_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec2& value) - { PROPERTIES_WIDGET(ImGui::InputFloat2(valueLabel, value_ptr(value), math::vec2_format_get(value))); }; - - auto float_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, float& value) - { PROPERTIES_WIDGET(ImGui::InputFloat(valueLabel, &value, STEP, STEP_FAST, math::float_format_get(value))); }; - - auto duration_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, int& value) + if (document) { - PROPERTIES_WIDGET( - input_int_range(valueLabel, value, anm2::FRAME_DURATION_MIN, anm2::FRAME_DURATION_MAX, STEP, STEP_FAST)); - }; - -#undef PROPERTIES_WIDGET - - float2_value("##Is Crop", localize.get(BASIC_CROP), isCrop, crop); - float2_value("##Is Size", localize.get(BASIC_SIZE), isSize, size); - float2_value("##Is Position", localize.get(BASIC_POSITION), isPosition, position); - float2_value("##Is Pivot", localize.get(BASIC_PIVOT), isPivot, pivot); - float2_value("##Is Scale", localize.get(BASIC_SCALE), isScale, scale); - float_value("##Is Rotation", localize.get(BASIC_ROTATION), isRotation, rotation); - duration_value("##Is Duration", localize.get(BASIC_DURATION), isDuration, duration); - color4_value("##Is Tint", localize.get(BASIC_TINT), isTint, tint); - color3_value("##Is Color Offset", localize.get(BASIC_COLOR_OFFSET), isColorOffset, colorOffset); - bool_value("##Is Visible", localize.get(BASIC_VISIBLE), isVisibleSet, isVisible); - ImGui::SameLine(); - bool_value("##Is Interpolated", localize.get(BASIC_INTERPOLATED), isInterpolatedSet, isInterpolated); - - auto frame_change = [&](anm2::ChangeType type) - { - anm2::FrameChange frameChange; - if (isCrop) frameChange.crop = std::make_optional(crop); - if (isSize) frameChange.size = std::make_optional(size); - if (isPosition) frameChange.position = std::make_optional(position); - if (isPivot) frameChange.pivot = std::make_optional(pivot); - if (isScale) frameChange.scale = std::make_optional(scale); - if (isRotation) frameChange.rotation = std::make_optional(rotation); - if (isDuration) frameChange.duration = std::make_optional(duration); - if (isTint) frameChange.tint = std::make_optional(tint); - if (isColorOffset) frameChange.colorOffset = std::make_optional(colorOffset); - if (isVisibleSet) frameChange.isVisible = std::make_optional(isVisible); - if (isInterpolatedSet) frameChange.isInterpolated = std::make_optional(isInterpolated); - - DOCUMENT_EDIT_PTR( - document, localize.get(EDIT_CHANGE_FRAME_PROPERTIES), Document::FRAMES, - item->frames_change(frameChange, type, *frames->selection.begin(), (int)frames->selection.size())); - - changePopup.close(); - }; - - bool isAnyProperty = isCrop || isSize || isPosition || isPivot || isScale || isRotation || isDuration || isTint || - isColorOffset || isVisibleSet || isInterpolatedSet; - - ImGui::Separator(); - - auto rowWidgetSize = widget_size_with_row_get(5); - - ImGui::BeginDisabled(!isAnyProperty); - - if (ImGui::Button(localize.get(LABEL_ADJUST), rowWidgetSize)) frame_change(anm2::ADJUST); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADJUST)); - - ImGui::SameLine(); - - if (ImGui::Button(localize.get(BASIC_ADD), rowWidgetSize)) frame_change(anm2::ADD); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADD_VALUES)); - - ImGui::SameLine(); - - if (ImGui::Button(localize.get(LABEL_SUBTRACT), rowWidgetSize)) frame_change(anm2::SUBTRACT); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SUBTRACT_VALUES)); - - ImGui::SameLine(); - - if (ImGui::Button(localize.get(LABEL_MULTIPLY), rowWidgetSize)) frame_change(anm2::MULTIPLY); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MULTIPLY_VALUES)); - - ImGui::SameLine(); - - if (ImGui::Button(localize.get(LABEL_DIVIDE), rowWidgetSize)) frame_change(anm2::DIVIDE); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_DIVIDE_VALUES)); - - ImGui::EndDisabled(); - + changeAllFrameProperties.update(*document, settings); + if (changeAllFrameProperties.isChanged) changePopup.close(); + } ImGui::EndPopup(); } configurePopup.trigger(); - if (ImGui::BeginPopupModal(configurePopup.label(), &configurePopup.isOpen, ImGuiWindowFlags_NoResize)) { - auto childSize = size_without_footer_get(2); - - if (ImGui::BeginTabBar("##Configure Tabs")) - { - if (ImGui::BeginTabItem(localize.get(LABEL_DISPLAY))) - { - if (ImGui::BeginChild("##Tab Child", childSize, true)) - { - ImGui::SeparatorText(localize.get(LABEL_WINDOW_MENU)); - input_float_range(localize.get(LABEL_UI_SCALE), editSettings.uiScale, 0.5f, 2.0f, 0.25f, 0.25f, "%.2f"); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_UI_SCALE)); - ImGui::Checkbox(localize.get(LABEL_VSYNC), &editSettings.isVsync); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_VSYNC)); - - ImGui::SeparatorText(localize.get(LABEL_LOCALIZATION)); - ImGui::Combo(localize.get(LABEL_LANGUAGE), &editSettings.language, LANGUAGE_STRINGS, LANGUAGE_COUNT); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LANGUAGE)); - - ImGui::SeparatorText(localize.get(LABEL_THEME)); - - for (int i = 0; i < theme::COUNT; i++) - { - ImGui::RadioButton(localize.get(theme::STRINGS[i]), &editSettings.theme, i); - ImGui::SameLine(); - } - } - ImGui::EndChild(); - - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem(localize.get(LABEL_FILE_MENU))) - { - if (ImGui::BeginChild("##Tab Child", childSize, true)) - { - ImGui::SeparatorText(localize.get(LABEL_AUTOSAVE)); - - ImGui::Checkbox(localize.get(BASIC_ENABLED), &editSettings.fileIsAutosave); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_AUTOSAVE_ENABLED)); - - ImGui::BeginDisabled(!editSettings.fileIsAutosave); - input_int_range(localize.get(LABEL_TIME_MINUTES), editSettings.fileAutosaveTime, 0, 10); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_AUTOSAVE_INTERVAL)); - ImGui::EndDisabled(); - - ImGui::SeparatorText(localize.get(LABEL_SNAPSHOTS)); - input_int_range(localize.get(LABEL_STACK_SIZE), editSettings.fileSnapshotStackSize, 0, 100); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_STACK_SIZE)); - - ImGui::SeparatorText(localize.get(LABEL_OPTIONS)); - ImGui::Checkbox(localize.get(LABEL_OVERWRITE_WARNING), &editSettings.fileIsWarnOverwrite); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OVERWRITE_WARNING)); - } - ImGui::EndChild(); - - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem(localize.get(LABEL_INPUT))) - { - if (ImGui::BeginChild("##Tab Child", childSize, true)) - { - ImGui::SeparatorText(localize.get(LABEL_KEYBOARD)); - - input_float_range(localize.get(LABEL_REPEAT_DELAY), editSettings.keyboardRepeatDelay, 0.05f, 1.0f, 0.05f, - 0.05f, "%.2f"); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPEAT_DELAY)); - - input_float_range(localize.get(LABEL_REPEAT_RATE), editSettings.keyboardRepeatRate, 0.005f, 1.0f, 0.005f, - 0.005f, "%.3f"); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPEAT_DELAY)); - - ImGui::SeparatorText(localize.get(LABEL_ZOOM)); - - input_float_range(localize.get(LABEL_ZOOM_STEP), editSettings.inputZoomStep, 10.0f, 250.0f, 10.0f, 10.0f, - "%.0f%%"); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ZOOM_STEP)); - - ImGui::SeparatorText(localize.get(LABEL_TOOL)); - - ImGui::Checkbox(localize.get(LABEL_MOVE_TOOL_SNAP), &editSettings.inputIsMoveToolSnapToMouse); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MOVE_TOOL_SNAP)); - } - ImGui::EndChild(); - - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem(localize.get(LABEL_SHORTCUTS_TAB))) - { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); - - if (ImGui::BeginChild("##Tab Child", childSize, true)) - { - if (ImGui::BeginTable(localize.get(LABEL_SHORTCUTS_TAB), 2, - ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY)) - { - ImGui::TableSetupScrollFreeze(0, 1); - ImGui::TableSetupColumn(localize.get(LABEL_SHORTCUT_COLUMN)); - ImGui::TableSetupColumn(localize.get(LABEL_VALUE_COLUMN)); - ImGui::TableHeadersRow(); - - for (int i = 0; i < SHORTCUT_COUNT; ++i) - { - bool isSelected = selectedShortcut == i; - - ShortcutMember member = SHORTCUT_MEMBERS[i]; - std::string* settingString = &(editSettings.*member); - std::string chordString = isSelected ? "" : *settingString; - - ImGui::PushID(i); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(localize.get(::anm2ed::SHORTCUT_STRING_TYPES[i])); - ImGui::TableSetColumnIndex(1); - - if (ImGui::Selectable(chordString.c_str(), isSelected)) selectedShortcut = i; - - ImGui::PopID(); - - if (isSelected) - { - ImGuiKeyChord chord{ImGuiKey_None}; - - if (ImGui::IsKeyDown(ImGuiMod_Ctrl)) chord |= ImGuiMod_Ctrl; - if (ImGui::IsKeyDown(ImGuiMod_Shift)) chord |= ImGuiMod_Shift; - if (ImGui::IsKeyDown(ImGuiMod_Alt)) chord |= ImGuiMod_Alt; - if (ImGui::IsKeyDown(ImGuiMod_Super)) chord |= ImGuiMod_Super; - - for (const auto& entry : KEY_MAP) - { - auto key = entry.second; - if (ImGui::IsKeyPressed(key)) - { - chord |= key; - *settingString = chord_to_string(chord); - selectedShortcut = -1; - break; - } - } - } - } - ImGui::EndTable(); - } - ImGui::EndChild(); - - ImGui::PopStyleVar(); - - ImGui::EndTabItem(); - } - } - - ImGui::EndTabBar(); - } - - auto widgetSize = widget_size_with_row_get(3); - - if (ImGui::Button(localize.get(BASIC_SAVE), widgetSize)) - { - settings = editSettings; - - ImGui::GetIO().KeyRepeatDelay = settings.keyboardRepeatDelay; - ImGui::GetIO().KeyRepeatRate = settings.keyboardRepeatRate; - ImGui::GetStyle().FontScaleMain = settings.uiScale; - SnapshotStack::max_size_set(settings.fileSnapshotStackSize); - imgui::theme_set((theme::Type)settings.theme); - localize.language = (Language)settings.language; - manager.chords_set(settings); - - for (auto& document : manager.documents) - document.snapshots.apply_limit(); - - configurePopup.close(); - } - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SETTINGS_SAVE)); - - ImGui::SameLine(); - - if (ImGui::Button(localize.get(LABEL_USE_DEFAULT_SETTINGS), widgetSize)) editSettings = Settings(); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_USE_DEFAULT_SETTINGS)); - - ImGui::SameLine(); - - if (ImGui::Button(localize.get(LABEL_CLOSE), widgetSize)) configurePopup.close(); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CLOSE_SETTINGS)); - + if (configurePopup.isJustOpened) configure.reset(settings); + configure.update(manager, settings); + if (configure.isSet) configurePopup.close(); ImGui::EndPopup(); } + configurePopup.end(); renderPopup.trigger(); - if (ImGui::BeginPopupModal(renderPopup.label(), &renderPopup.isOpen, ImGuiWindowFlags_NoResize)) { - auto& ffmpegPath = settings.renderFFmpegPath; - auto& path = settings.renderPath; - auto& format = settings.renderFormat; - auto& scale = settings.renderScale; - auto& isRaw = settings.renderIsRawAnimation; - auto& type = settings.renderType; - auto& start = manager.recordingStart; - auto& end = manager.recordingEnd; - auto& rows = settings.renderRows; - auto& columns = settings.renderColumns; - auto& isRange = manager.isRecordingRange; - auto& frames = document->frames.selection; - auto& reference = document->reference; - int length = std::max(1, end - start + 1); - - auto range_to_frames_set = [&]() + if (document) { - if (auto item = document->item_get()) - { - int duration{}; - for (auto [i, frame] : std::views::enumerate(item->frames)) - { - if ((int)i == *frames.begin()) start = duration; - if ((int)i == *frames.rbegin()) end = duration + frame.duration - 1; - - duration += frame.duration; - } - } - }; - - auto range_to_animation_set = [&]() - { - start = 0; - end = animation->frameNum - 1; - }; - - auto range_set = [&]() - { - if (!isRange) range_to_animation_set(); - length = std::max(1, end - (start + 1)); - }; - - auto rows_columns_set = [&]() - { - auto framesNeeded = std::max(1, length); - int bestRows = 1; - int bestColumns = framesNeeded; - - auto bestScore = std::make_tuple(bestColumns - bestRows, bestColumns * bestRows - framesNeeded, -bestColumns); - - for (int candidateRows = 1; candidateRows <= framesNeeded; ++candidateRows) - { - int candidateColumns = (framesNeeded + candidateRows - 1) / candidateRows; - if (candidateColumns < candidateRows) break; - - auto candidateScore = std::make_tuple(candidateColumns - candidateRows, - candidateColumns * candidateRows - framesNeeded, -candidateColumns); - - if (candidateScore < bestScore) - { - bestScore = candidateScore; - bestRows = candidateRows; - bestColumns = candidateColumns; - } - } - - rows = bestRows; - columns = bestColumns; - }; - - auto replace_extension = [&]() - { path = std::filesystem::path(path).replace_extension(render::EXTENSIONS[type]).string(); }; - - auto render_set = [&]() - { - replace_extension(); - range_set(); - rows_columns_set(); - }; - - auto widgetSize = widget_size_with_row_get(2); - auto dialogType = type == render::PNGS ? dialog::PNG_DIRECTORY_SET - : type == render::SPRITESHEET ? dialog::PNG_PATH_SET - : type == render::GIF ? dialog::GIF_PATH_SET - : type == render::WEBM ? dialog::WEBM_PATH_SET - : dialog::NONE; - - if (renderPopup.isJustOpened) render_set(); - - if (ImGui::ImageButton("##FFmpeg Path Set", resources.icons[icon::FOLDER].id, icon_size_get())) - dialog.file_open(dialog::FFMPEG_PATH_SET); - ImGui::SameLine(); - input_text_string(localize.get(LABEL_FFMPEG_PATH), &ffmpegPath); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FFMPEG_PATH)); - - if (dialog.is_selected(dialog::FFMPEG_PATH_SET)) - { - ffmpegPath = filesystem::path_to_utf8(dialog.path); - dialog.reset(); + if (renderPopup.isJustOpened) renderAnimation.reset(manager, *document, settings); + renderAnimation.update(manager, *document, resources, settings, dialog); } - - if (ImGui::ImageButton("##Path Set", resources.icons[icon::FOLDER].id, icon_size_get())) - { - if (dialogType == dialog::PNG_DIRECTORY_SET) - dialog.folder_open(dialogType); - else - dialog.file_save(dialogType); - } - ImGui::SameLine(); - auto pathLabel = type == render::PNGS ? LABEL_OUTPUT_DIRECTORY : LABEL_OUTPUT_PATH; - input_text_string(localize.get(pathLabel), &path); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OUTPUT_PATH)); - - if (dialog.is_selected(dialogType)) - { - path = filesystem::path_to_utf8(dialog.path); - dialog.reset(); - } - - if (ImGui::Combo(localize.get(LABEL_TYPE), &type, render::STRINGS, render::COUNT)) render_set(); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RENDER_TYPE)); - - if (type == render::PNGS || type == render::SPRITESHEET) ImGui::Separator(); - - if (type == render::PNGS) - { - if (input_text_string(localize.get(LABEL_FORMAT), &format)) - format = std::filesystem::path(format).replace_extension(".png").string(); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FORMAT)); - } - else if (type == render::SPRITESHEET) - { - input_int_range(localize.get(LABEL_GENERATE_ROWS), rows, 1, length); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ROWS)); - - input_int_range(localize.get(LABEL_GENERATE_COLUMNS), columns, 1, length); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COLUMNS)); - - if (ImGui::Button(localize.get(LABEL_SET_TO_RECOMMENDED))) rows_columns_set(); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SET_TO_RECOMMENDED)); - } - - ImGui::Separator(); - - if (ImGui::Checkbox(localize.get(LABEL_CUSTOM_RANGE), &isRange)) - { - range_set(); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CUSTOM_RANGE)); - } - - ImGui::SameLine(); - - ImGui::BeginDisabled(frames.empty() || reference.itemID == anm2::TRIGGER); - if (ImGui::Button(localize.get(LABEL_TO_SELECTED_FRAMES))) range_to_frames_set(); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TO_SELECTED_FRAMES)); - ImGui::EndDisabled(); - - ImGui::SameLine(); - - if (ImGui::Button(localize.get(LABEL_TO_ANIMATION_RANGE))) range_to_animation_set(); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TO_ANIMATION_RANGE)); - - ImGui::BeginDisabled(!isRange); - { - input_int_range(localize.get(LABEL_START), start, 0, animation->frameNum); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_START)); - input_int_range(localize.get(LABEL_END), end, start, animation->frameNum); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_END)); - } - ImGui::EndDisabled(); - - ImGui::Separator(); - - ImGui::Checkbox(localize.get(LABEL_RAW), &isRaw); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RAW)); - - ImGui::BeginDisabled(!isRaw); - { - input_float_range(localize.get(BASIC_SCALE), scale, 1.0f, 100.0f, STEP, STEP_FAST, "%.1fx"); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SCALE_OUTPUT)); - } - ImGui::EndDisabled(); - - ImGui::Separator(); - - ImGui::Checkbox(localize.get(LABEL_SOUND), &settings.timelineIsSound); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SOUND)); - - ImGui::Separator(); - - if (ImGui::Button(localize.get(LABEL_RENDER), widgetSize)) - { - replace_extension(); - - bool isRender = true; - if (!ffmpeg_is_executable(ffmpegPath)) - { - toasts.push(localize.get(TOAST_INVALID_FFMPEG_PATH)); - logger.error(localize.get(TOAST_INVALID_FFMPEG_PATH, anm2ed::ENGLISH)); - isRender = false; - } - - if (isRender && type == render::PNGS) isRender = png_directory_ensure(path); - - if (isRender) - { - manager.isRecordingStart = true; - manager.progressPopup.open(); - } - - renderPopup.close(); - } - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RENDER_BUTTON)); - - ImGui::SameLine(); - - if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) renderPopup.close(); - + if (renderAnimation.isEnd) renderPopup.close(); ImGui::EndPopup(); } - renderPopup.end(); aboutPopup.trigger(); - if (ImGui::BeginPopupModal(aboutPopup.label(), &aboutPopup.isOpen, ImGuiWindowFlags_NoResize)) { - static CreditsState creditsState{}; - - auto credits_reset = [&]() - { - resources.music_track().play(true); - creditsState = {}; - creditsState.spawnTimer = CREDIT_DELAY; - }; - - if (aboutPopup.isJustOpened) credits_reset(); - - auto size = ImGui::GetContentRegionAvail(); - auto applicationLabel = localize.get(LABEL_APPLICATION_NAME); - auto versionLabel = localize.get(LABEL_APPLICATION_VERSION); - - ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE_LARGE); - - ImGui::SetCursorPosX((size.x - ImGui::CalcTextSize(applicationLabel).x) / 2); - ImGui::TextUnformatted(applicationLabel); - - ImGui::SetCursorPosX((size.x - ImGui::CalcTextSize(versionLabel).x) / 2); - ImGui::TextUnformatted(versionLabel); - - ImGui::PopFont(); - - auto creditRegionPos = ImGui::GetCursorScreenPos(); - auto creditRegionSize = ImGui::GetContentRegionAvail(); - - if (creditRegionSize.y > 0.0f && creditRegionSize.x > 0.0f) - { - auto fontSize = ImGui::GetFontSize(); - auto drawList = ImGui::GetWindowDrawList(); - auto clipMax = ImVec2(creditRegionPos.x + creditRegionSize.x, creditRegionPos.y + creditRegionSize.y); - drawList->PushClipRect(creditRegionPos, clipMax, true); - - auto delta = ImGui::GetIO().DeltaTime; - creditsState.spawnTimer -= delta; - auto maxVisible = std::max(1, (int)std::floor(creditRegionSize.y / (float)fontSize)); - - while (creditsState.active.size() < (size_t)maxVisible && creditsState.spawnTimer <= 0.0f) - { - creditsState.active.push_back({creditsState.nextIndex, 0.0f}); - creditsState.nextIndex = (creditsState.nextIndex + 1) % CREDIT_COUNT; - creditsState.spawnTimer += CREDIT_DELAY; - } - - auto baseY = clipMax.y - (float)fontSize; - const auto& baseColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); - auto fadeSpan = (float)fontSize * 2.0f; - - for (auto it = creditsState.active.begin(); it != creditsState.active.end();) - { - it->offset += CREDIT_SCROLL_SPEED * delta; - auto yPos = baseY - it->offset; - if (yPos + fontSize < creditRegionPos.y) - { - it = creditsState.active.erase(it); - continue; - } - - const auto& credit = CREDITS[it->index]; - auto fontPtr = resources.fonts[credit.font].get(); - auto textSize = fontPtr->CalcTextSizeA((float)fontSize, FLT_MAX, 0.0f, credit.string); - auto xPos = creditRegionPos.x + (creditRegionSize.x - textSize.x) * 0.5f; - - auto alpha = 1.0f; - auto topDist = yPos - creditRegionPos.y; - if (topDist < fadeSpan) alpha *= std::clamp(topDist / fadeSpan, 0.0f, 1.0f); - auto bottomDist = (creditRegionPos.y + creditRegionSize.y) - (yPos + fontSize); - if (bottomDist < fadeSpan) alpha *= std::clamp(bottomDist / fadeSpan, 0.0f, 1.0f); - if (alpha <= 0.0f) - { - ++it; - continue; - } - - auto color = baseColor; - color.w *= alpha; - - drawList->AddText(fontPtr, fontSize, ImVec2(xPos, yPos), ImGui::GetColorU32(color), credit.string); - ++it; - } - - drawList->PopClipRect(); - } - + if (aboutPopup.isJustOpened) about.reset(resources); + about.update(resources); ImGui::EndPopup(); } @@ -1121,8 +221,8 @@ namespace anm2ed::imgui aboutPopup.end(); - if (shortcut(manager.chords[SHORTCUT_NEW], shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_NEW); - if (shortcut(manager.chords[SHORTCUT_OPEN], shortcut::GLOBAL)) dialog.file_open(dialog::ANM2_OPEN); + if (shortcut(manager.chords[SHORTCUT_NEW], shortcut::GLOBAL)) dialog.file_save(Dialog::ANM2_NEW); + if (shortcut(manager.chords[SHORTCUT_OPEN], shortcut::GLOBAL)) dialog.file_open(Dialog::ANM2_OPEN); if (shortcut(manager.chords[SHORTCUT_SAVE], shortcut::GLOBAL)) { if (settings.fileIsWarnOverwrite) @@ -1130,7 +230,7 @@ namespace anm2ed::imgui else manager.save(); } - if (shortcut(manager.chords[SHORTCUT_SAVE_AS], shortcut::GLOBAL)) dialog.file_save(dialog::ANM2_SAVE); + if (shortcut(manager.chords[SHORTCUT_SAVE_AS], shortcut::GLOBAL)) dialog.file_save(Dialog::ANM2_SAVE); if (shortcut(manager.chords[SHORTCUT_EXIT], shortcut::GLOBAL)) isQuitting = true; } } diff --git a/src/imgui/taskbar.h b/src/imgui/taskbar.h index 6de051f..cd171e7 100644 --- a/src/imgui/taskbar.h +++ b/src/imgui/taskbar.h @@ -8,12 +8,23 @@ #include "settings.h" #include "strings.h" +#include "wizard/about.h" +#include "wizard/change_all_frame_properties.h" +#include "wizard/configure.h" +#include "wizard/generate_animation_from_grid.h" +#include "wizard/render_animation.h" + namespace anm2ed::imgui { class Taskbar { + wizard::ChangeAllFrameProperties changeAllFrameProperties{}; + wizard::About about{}; + wizard::Configure configure{}; + wizard::GenerateAnimationFromGrid generateAnimationFromGrid{}; + wizard::RenderAnimation renderAnimation{}; + Canvas generate; - float generateTime{}; PopupHelper generatePopup{PopupHelper(LABEL_TASKBAR_GENERATE_ANIMATION_FROM_GRID)}; PopupHelper changePopup{PopupHelper(LABEL_CHANGE_ALL_FRAME_PROPERTIES, imgui::POPUP_NORMAL_NO_HEIGHT)}; PopupHelper overwritePopup{PopupHelper(LABEL_TASKBAR_OVERWRITE_FILE, imgui::POPUP_SMALL_NO_HEIGHT)}; @@ -21,14 +32,11 @@ namespace anm2ed::imgui PopupHelper configurePopup{PopupHelper(LABEL_TASKBAR_CONFIGURE)}; PopupHelper aboutPopup{PopupHelper(LABEL_TASKBAR_ABOUT)}; Settings editSettings{}; - int selectedShortcut{-1}; - int creditsIndex{}; bool isQuittingMode{}; public: float height{}; - Taskbar(); void update(Manager&, Settings&, Resources&, Dialog&, bool&); }; }; diff --git a/src/imgui/window/animation_preview.cpp b/src/imgui/window/animation_preview.cpp index 6290587..0996a54 100644 --- a/src/imgui/window/animation_preview.cpp +++ b/src/imgui/window/animation_preview.cpp @@ -11,12 +11,12 @@ #include "imgui_.h" #include "log.h" #include "math_.h" +#include "path_.h" #include "strings.h" #include "toast.h" #include "tool.h" #include "types.h" -using namespace anm2ed::canvas; using namespace anm2ed::types; using namespace anm2ed::util; using namespace anm2ed::resource; @@ -48,6 +48,7 @@ namespace anm2ed::imgui { auto& ffmpegPath = settings.renderFFmpegPath; auto& path = settings.renderPath; + auto pathString = path::to_utf8(path); auto& type = settings.renderType; if (playback.time > end || playback.isFinished) @@ -55,11 +56,11 @@ namespace anm2ed::imgui if (type == render::PNGS) { auto& format = settings.renderFormat; + auto formatString = path::to_utf8(format); bool isSuccess{true}; for (auto [i, frame] : std::views::enumerate(renderFrames)) { - std::filesystem::path outputPath = - std::filesystem::path(path) / std::vformat(format, std::make_format_args(i)); + auto outputPath = path / std::vformat(formatString, std::make_format_args(i)); if (!frame.write_png(outputPath)) { @@ -71,15 +72,16 @@ namespace anm2ed::imgui if (isSuccess) { - toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES), std::make_format_args(path))); - logger.info( - std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES, anm2ed::ENGLISH), std::make_format_args(path))); + toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES), std::make_format_args(pathString))); + logger.info(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES, anm2ed::ENGLISH), + std::make_format_args(pathString))); } else { - toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES_FAILED), std::make_format_args(path))); + toasts.push( + std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES_FAILED), std::make_format_args(pathString))); logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_FRAMES_FAILED, anm2ed::ENGLISH), - std::make_format_args(path))); + std::make_format_args(pathString))); } } else if (type == render::SPRITESHEET) @@ -129,15 +131,16 @@ namespace anm2ed::imgui Texture spritesheetTexture(spritesheet.data(), spritesheetSize); if (spritesheetTexture.write_png(path)) { - toasts.push(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET), std::make_format_args(path))); - logger.info( - std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET, anm2ed::ENGLISH), std::make_format_args(path))); + toasts.push(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET), std::make_format_args(pathString))); + logger.info(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET, anm2ed::ENGLISH), + std::make_format_args(pathString))); } else { - toasts.push(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET_FAILED), std::make_format_args(path))); + toasts.push( + std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET_FAILED), std::make_format_args(pathString))); logger.error(std::vformat(localize.get(TOAST_EXPORT_SPRITESHEET_FAILED, anm2ed::ENGLISH), - std::make_format_args(path))); + std::make_format_args(pathString))); } } } @@ -146,16 +149,16 @@ namespace anm2ed::imgui { if (animation_render(ffmpegPath, path, renderFrames, audioStream, (render::Type)type, size, anm2.info.fps)) { - toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION), std::make_format_args(path))); + toasts.push(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION), std::make_format_args(pathString))); logger.info(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION, anm2ed::ENGLISH), - std::make_format_args(path))); + std::make_format_args(pathString))); } else { toasts.push( - std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED), std::make_format_args(path))); + std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED), std::make_format_args(pathString))); logger.error(std::vformat(localize.get(TOAST_EXPORT_RENDERED_ANIMATION_FAILED, anm2ed::ENGLISH), - std::make_format_args(path))); + std::make_format_args(pathString))); } } @@ -163,7 +166,6 @@ namespace anm2ed::imgui if (settings.renderIsRawAnimation) { - settings = savedSettings; pan = savedPan; @@ -290,8 +292,12 @@ namespace anm2ed::imgui auto zoom_in = [&]() { zoom_adjust(zoomStep); }; auto zoom_out = [&]() { zoom_adjust(-zoomStep); }; + manager.isAbleToRecord = false; + if (ImGui::Begin(localize.get(LABEL_ANIMATION_PREVIEW_WINDOW), &settings.windowIsAnimationPreview)) { + manager.isAbleToRecord = true; + auto childSize = ImVec2(row_widget_width_get(4), (ImGui::GetTextLineHeightWithSpacing() * 4) + (ImGui::GetStyle().WindowPadding.y * 2)); @@ -402,6 +408,7 @@ namespace anm2ed::imgui settings.previewBackgroundColor = vec4(); settings.previewIsGrid = false; settings.previewIsAxes = false; + settings.previewIsPivots = false; settings.previewIsBorder = false; settings.timelineIsOnlyShowLayers = true; settings.onionskinIsEnabled = false; @@ -729,7 +736,7 @@ namespace anm2ed::imgui auto frame = document.frame_get(); auto useTool = tool; - auto step = isMod ? canvas::STEP_FAST : canvas::STEP; + auto step = isMod ? STEP_FAST : STEP; mousePos = position_translate(zoom, pan, to_vec2(ImGui::GetMousePos()) - to_vec2(cursorScreenPos)); if (isMouseMiddleDown) useTool = tool::PAN; diff --git a/src/imgui/window/frame_properties.cpp b/src/imgui/window/frame_properties.cpp index 6b68fc3..aabceb4 100644 --- a/src/imgui/window/frame_properties.cpp +++ b/src/imgui/window/frame_properties.cpp @@ -13,7 +13,6 @@ using namespace glm; namespace anm2ed::imgui { - void FrameProperties::update(Manager& manager, Settings& settings) { if (ImGui::Begin(localize.get(LABEL_FRAME_PROPERTIES_WINDOW), &settings.windowIsFrameProperties)) @@ -180,151 +179,16 @@ namespace anm2ed::imgui ImGui::EndDisabled(); } else - { - auto& isCrop = settings.changeIsCrop; - auto& isCropX = settings.changeIsCropX; - auto& isCropY = settings.changeIsCropY; - auto& isSize = settings.changeIsSize; - auto& isPosition = settings.changeIsPosition; - auto& isPivot = settings.changeIsPivot; - auto& isScale = settings.changeIsScale; - auto& isRotation = settings.changeIsRotation; - auto& isDuration = settings.changeIsDuration; - auto& isTint = settings.changeIsTint; - auto& isColorOffset = settings.changeIsColorOffset; - auto& isVisibleSet = settings.changeIsVisibleSet; - auto& isInterpolatedSet = settings.changeIsInterpolatedSet; - auto& crop = settings.changeCrop; - auto& size = settings.changeSize; - auto& position = settings.changePosition; - auto& pivot = settings.changePivot; - auto& scale = settings.changeScale; - auto& rotation = settings.changeRotation; - auto& duration = settings.changeDuration; - auto& tint = settings.changeTint; - auto& colorOffset = settings.changeColorOffset; - auto& isVisible = settings.changeIsVisible; - auto& isInterpolated = settings.changeIsInterpolated; - -#define PROPERTIES_WIDGET(body, checkboxLabel, isEnabled) \ - ImGui::Checkbox(checkboxLabel, &isEnabled); \ - ImGui::SameLine(); \ - ImGui::BeginDisabled(!isEnabled); \ - body; \ - ImGui::EndDisabled(); - - auto bool_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, bool& value) - { PROPERTIES_WIDGET(ImGui::Checkbox(valueLabel, &value), checkboxLabel, isEnabled) }; - - auto color3_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec3& value) - { PROPERTIES_WIDGET(ImGui::ColorEdit3(valueLabel, value_ptr(value)), checkboxLabel, isEnabled); }; - - auto color4_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec4& value) - { PROPERTIES_WIDGET(ImGui::ColorEdit4(valueLabel, value_ptr(value)), checkboxLabel, isEnabled); }; - - auto float2_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, vec2& value) - { - PROPERTIES_WIDGET(ImGui::InputFloat2(valueLabel, value_ptr(value), vec2_format_get(value)), checkboxLabel, - isEnabled); - }; - - auto float2_value_new = [&](const char* checkboxXLabel, const char* checkboxYLabel, const char* valueXLabel, - const char* valueYLabel, bool& isXEnabled, bool& isYEnabled, vec2& value) - { - auto width = - (ImGui::CalcItemWidth() - ImGui::GetTextLineHeight() - (ImGui::GetStyle().ItemInnerSpacing.x * 6)) / 2; - - PROPERTIES_WIDGET(ImGui::PushItemWidth(width); - ImGui::DragFloat(valueXLabel, &value.x, DRAG_SPEED, 0.0f, 0.0f, float_format_get(value.x)); - ImGui::PopItemWidth(), checkboxXLabel, isXEnabled); - ImGui::SameLine(); - PROPERTIES_WIDGET(ImGui::PushItemWidth(width); - ImGui::DragFloat(valueYLabel, &value.y, DRAG_SPEED, 0.0f, 0.0f, float_format_get(value.y)); - ImGui::PopItemWidth(), checkboxYLabel, isYEnabled); - }; - - auto float_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, float& value) - { - PROPERTIES_WIDGET(ImGui::InputFloat(valueLabel, &value, STEP, STEP_FAST, float_format_get(value)), - checkboxLabel, isEnabled); - }; - - auto duration_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, int& value) - { - PROPERTIES_WIDGET( - input_int_range(valueLabel, value, anm2::FRAME_DURATION_MIN, anm2::FRAME_DURATION_MAX, STEP, STEP_FAST), - checkboxLabel, isEnabled); - }; - -#undef PROPERTIES_WIDGET - - float2_value_new("##Is Crop X", "##Is Crop Y", "##Crop X", localize.get(BASIC_CROP), isCropX, isCropY, crop); - float2_value("##Is Size", localize.get(BASIC_SIZE), isSize, size); - float2_value("##Is Position", localize.get(BASIC_POSITION), isPosition, position); - float2_value("##Is Pivot", localize.get(BASIC_PIVOT), isPivot, pivot); - float2_value("##Is Scale", localize.get(BASIC_SCALE), isScale, scale); - float_value("##Is Rotation", localize.get(BASIC_ROTATION), isRotation, rotation); - duration_value("##Is Duration", localize.get(BASIC_DURATION), isDuration, duration); - color4_value("##Is Tint", localize.get(BASIC_TINT), isTint, tint); - color3_value("##Is Color Offset", localize.get(BASIC_COLOR_OFFSET), isColorOffset, colorOffset); - bool_value("##Is Visible", localize.get(BASIC_VISIBLE), isVisibleSet, isVisible); - ImGui::SameLine(); - bool_value("##Is Interpolated", localize.get(BASIC_INTERPOLATED), isInterpolatedSet, isInterpolated); - - auto frame_change = [&](anm2::ChangeType type) - { - anm2::FrameChange frameChange; - if (isCrop) frameChange.crop = std::make_optional(crop); - if (isSize) frameChange.size = std::make_optional(size); - if (isPosition) frameChange.position = std::make_optional(position); - if (isPivot) frameChange.pivot = std::make_optional(pivot); - if (isScale) frameChange.scale = std::make_optional(scale); - if (isRotation) frameChange.rotation = std::make_optional(rotation); - if (isDuration) frameChange.duration = std::make_optional(duration); - if (isTint) frameChange.tint = std::make_optional(tint); - if (isColorOffset) frameChange.colorOffset = std::make_optional(colorOffset); - if (isVisibleSet) frameChange.isVisible = std::make_optional(isVisible); - if (isInterpolatedSet) frameChange.isInterpolated = std::make_optional(isInterpolated); - - DOCUMENT_EDIT(document, localize.get(EDIT_CHANGE_FRAME_PROPERTIES), Document::FRAMES, - document.item_get()->frames_change(frameChange, type, *frames.begin(), (int)frames.size())); - }; - - ImGui::Separator(); - - bool isAnyProperty = isCrop || isSize || isPosition || isPivot || isScale || isRotation || isDuration || - isTint || isColorOffset || isVisibleSet || isInterpolatedSet; - - auto rowWidgetSize = widget_size_with_row_get(5); - - ImGui::BeginDisabled(!isAnyProperty); - - if (ImGui::Button(localize.get(LABEL_ADJUST), rowWidgetSize)) frame_change(anm2::ADJUST); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADJUST)); - - ImGui::SameLine(); - - if (ImGui::Button(localize.get(BASIC_ADD), rowWidgetSize)) frame_change(anm2::ADD); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADD_VALUES)); - - ImGui::SameLine(); - - if (ImGui::Button(localize.get(LABEL_SUBTRACT), rowWidgetSize)) frame_change(anm2::SUBTRACT); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SUBTRACT_VALUES)); - - ImGui::SameLine(); - - if (ImGui::Button(localize.get(LABEL_MULTIPLY), rowWidgetSize)) frame_change(anm2::MULTIPLY); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MULTIPLY_VALUES)); - - ImGui::SameLine(); - - if (ImGui::Button(localize.get(LABEL_DIVIDE), rowWidgetSize)) frame_change(anm2::DIVIDE); - ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_DIVIDE_VALUES)); - - ImGui::EndDisabled(); - } + changeAllFrameProperties.update(document, settings); } ImGui::End(); + + dummy_value_negative() = -1; + dummy_value() = 0; + dummy_value() = 0; + dummy_value() = 0; + dummy_value() = vec2(); + dummy_value() = vec3(); + dummy_value() = vec4(); } } diff --git a/src/imgui/window/frame_properties.h b/src/imgui/window/frame_properties.h index eb2ef2c..a5d6640 100644 --- a/src/imgui/window/frame_properties.h +++ b/src/imgui/window/frame_properties.h @@ -3,23 +3,15 @@ #include #include "manager.h" -#include "settings.h" - -namespace anm2 -{ - struct Frame; -} +#include "wizard/change_all_frame_properties.h" namespace anm2ed::imgui { class FrameProperties { + wizard::ChangeAllFrameProperties changeAllFrameProperties{}; + public: void update(Manager&, Settings&); - - private: - glm::vec2 cropEditingValue{}; - const anm2::Frame* cropEditingFrame{}; - bool isCropEditing{}; }; } diff --git a/src/imgui/window/sounds.cpp b/src/imgui/window/sounds.cpp index 0e13fd3..0aa9aca 100644 --- a/src/imgui/window/sounds.cpp +++ b/src/imgui/window/sounds.cpp @@ -2,12 +2,11 @@ #include -#include "filesystem_.h" #include "log.h" +#include "path_.h" #include "strings.h" #include "toast.h" -using namespace anm2ed::dialog; using namespace anm2ed::util; using namespace anm2ed::types; using namespace anm2ed::resource; @@ -24,8 +23,8 @@ namespace anm2ed::imgui auto& selection = document.sound.selection; auto style = ImGui::GetStyle(); - auto add_open = [&]() { dialog.file_open(dialog::SOUND_OPEN); }; - auto replace_open = [&]() { dialog.file_open(dialog::SOUND_REPLACE); }; + auto add_open = [&]() { dialog.file_open(Dialog::SOUND_OPEN); }; + auto replace_open = [&]() { dialog.file_open(Dialog::SOUND_REPLACE); }; auto play = [&](anm2::Sound& sound) { sound.play(); }; @@ -34,7 +33,7 @@ namespace anm2ed::imgui auto behavior = [&]() { int id{}; - auto pathString = filesystem::path_to_utf8(path); + auto pathString = path::to_utf8(path); if (anm2.sound_add(document.directory_get(), path, id)) { selection = {id}; @@ -76,7 +75,7 @@ namespace anm2ed::imgui { anm2::Sound& sound = anm2.content.sounds[id]; sound.reload(document.directory_get()); - auto pathString = filesystem::path_to_utf8(sound.path); + auto pathString = path::to_utf8(sound.path); toasts.push(std::vformat(localize.get(TOAST_RELOAD_SOUND), std::make_format_args(id, pathString))); logger.info( std::vformat(localize.get(TOAST_RELOAD_SOUND, anm2ed::ENGLISH), std::make_format_args(id, pathString))); @@ -95,7 +94,7 @@ namespace anm2ed::imgui auto& id = *selection.begin(); anm2::Sound& sound = anm2.content.sounds[id]; sound = anm2::Sound(document.directory_get(), path); - auto pathString = filesystem::path_to_utf8(sound.path); + auto pathString = path::to_utf8(sound.path); toasts.push(std::vformat(localize.get(TOAST_REPLACE_SOUND), std::make_format_args(id, pathString))); logger.info( std::vformat(localize.get(TOAST_REPLACE_SOUND, anm2ed::ENGLISH), std::make_format_args(id, pathString))); @@ -215,7 +214,7 @@ namespace anm2ed::imgui bool isValid = sound.is_valid(); auto& soundIcon = isValid ? resources.icons[icon::SOUND] : resources.icons[icon::NONE]; auto tintColor = !isValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); - auto pathString = filesystem::path_to_utf8(sound.path); + auto pathString = path::to_utf8(sound.path); ImGui::SetNextItemSelectionUserData(id); ImGui::SetNextItemStorageID(id); @@ -271,9 +270,9 @@ namespace anm2ed::imgui ImGui::EndChild(); ImGui::PopID(); - } - context_menu(); + context_menu(); + } ImGui::PopStyleVar(); selection.finish(); @@ -288,7 +287,7 @@ namespace anm2ed::imgui if (ImGui::Button(localize.get(BASIC_ADD), widgetSize)) add_open(); imgui::set_item_tooltip_shortcut(localize.get(TOOLTIP_SOUND_ADD), settings.shortcutAdd); - if (dialog.is_selected(dialog::SOUND_OPEN)) + if (dialog.is_selected(Dialog::SOUND_OPEN)) { add(dialog.path); dialog.reset(); @@ -316,7 +315,7 @@ namespace anm2ed::imgui ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPLACE_SOUND)); ImGui::EndDisabled(); - if (dialog.is_selected(dialog::SOUND_REPLACE)) + if (dialog.is_selected(Dialog::SOUND_REPLACE)) { replace(dialog.path); dialog.reset(); diff --git a/src/imgui/window/spritesheet_editor.cpp b/src/imgui/window/spritesheet_editor.cpp index 0bc3ad9..5f786c9 100644 --- a/src/imgui/window/spritesheet_editor.cpp +++ b/src/imgui/window/spritesheet_editor.cpp @@ -11,7 +11,6 @@ #include "tool.h" #include "types.h" -using namespace anm2ed::canvas; using namespace anm2ed::types; using namespace anm2ed::resource; using namespace anm2ed::util; @@ -19,12 +18,6 @@ using namespace glm; namespace anm2ed::imgui { - constexpr auto BORDER_DASH_LENGTH = 1.0f; - constexpr auto BORDER_DASH_GAP = 0.5f; - constexpr auto BORDER_DASH_OFFSET = 0.0f; - - constexpr auto PIVOT_COLOR = color::PINK; - SpritesheetEditor::SpritesheetEditor() : Canvas(vec2()) {} void SpritesheetEditor::update(Manager& manager, Settings& settings, Resources& resources) @@ -218,7 +211,7 @@ namespace anm2ed::imgui rect_render(dashedShader, cropTransform, cropModel, color::RED); auto pivotTransform = - transform * math::quad_model_get(canvas::PIVOT_SIZE, frame->crop + frame->pivot, PIVOT_SIZE * 0.5f); + transform * math::quad_model_get(PIVOT_SIZE, frame->crop + frame->pivot, PIVOT_SIZE * 0.5f); texture_render(shaderTexture, resources.icons[icon::PIVOT].id, pivotTransform, PIVOT_COLOR); } } @@ -278,7 +271,7 @@ namespace anm2ed::imgui auto frame = document.frame_get(); auto useTool = tool; - auto step = isMod ? canvas::STEP_FAST : canvas::STEP; + auto step = isMod ? STEP_FAST : STEP; auto stepX = isGridSnap ? step * gridSize.x : step; auto stepY = isGridSnap ? step * gridSize.y : step; previousMousePos = mousePos; diff --git a/src/imgui/window/spritesheets.cpp b/src/imgui/window/spritesheets.cpp index 9691804..5f9f17c 100644 --- a/src/imgui/window/spritesheets.cpp +++ b/src/imgui/window/spritesheets.cpp @@ -6,8 +6,8 @@ #include #include "document.h" -#include "filesystem_.h" #include "log.h" +#include "path_.h" #include "strings.h" #include "toast.h" @@ -28,8 +28,8 @@ namespace anm2ed::imgui auto& reference = document.spritesheet.reference; auto style = ImGui::GetStyle(); - auto add_open = [&]() { dialog.file_open(dialog::SPRITESHEET_OPEN); }; - auto replace_open = [&]() { dialog.file_open(dialog::SPRITESHEET_REPLACE); }; + auto add_open = [&]() { dialog.file_open(Dialog::SPRITESHEET_OPEN); }; + auto replace_open = [&]() { dialog.file_open(Dialog::SPRITESHEET_REPLACE); }; auto add = [&](const std::filesystem::path& path) { @@ -47,7 +47,7 @@ namespace anm2ed::imgui for (auto& id : unused) { anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; - auto pathString = filesystem::path_to_utf8(spritesheet.path); + auto pathString = path::to_utf8(spritesheet.path); toasts.push(std::vformat(localize.get(TOAST_REMOVE_SPRITESHEET), std::make_format_args(id, pathString))); logger.info(std::vformat(localize.get(TOAST_REMOVE_SPRITESHEET, anm2ed::ENGLISH), std::make_format_args(id, pathString))); @@ -69,7 +69,7 @@ namespace anm2ed::imgui { anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; spritesheet.reload(document.directory_get()); - auto pathString = filesystem::path_to_utf8(spritesheet.path); + auto pathString = path::to_utf8(spritesheet.path); toasts.push(std::vformat(localize.get(TOAST_RELOAD_SPRITESHEET), std::make_format_args(id, pathString))); logger.info(std::vformat(localize.get(TOAST_RELOAD_SPRITESHEET, anm2ed::ENGLISH), std::make_format_args(id, pathString))); @@ -88,7 +88,7 @@ namespace anm2ed::imgui auto& id = *selection.begin(); anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; spritesheet = anm2::Spritesheet(document.directory_get(), path); - auto pathString = filesystem::path_to_utf8(spritesheet.path); + auto pathString = path::to_utf8(spritesheet.path); toasts.push(std::vformat(localize.get(TOAST_REPLACE_SPRITESHEET), std::make_format_args(id, pathString))); logger.info(std::vformat(localize.get(TOAST_REPLACE_SPRITESHEET, anm2ed::ENGLISH), std::make_format_args(id, pathString))); @@ -104,7 +104,7 @@ namespace anm2ed::imgui for (auto& id : selection) { anm2::Spritesheet& spritesheet = anm2.content.spritesheets[id]; - auto pathString = filesystem::path_to_utf8(spritesheet.path); + auto pathString = path::to_utf8(spritesheet.path); if (spritesheet.save(document.directory_get())) { toasts.push(std::vformat(localize.get(TOAST_SAVE_SPRITESHEET), std::make_format_args(id, pathString))); @@ -126,10 +126,9 @@ namespace anm2ed::imgui std::error_code ec{}; auto absolutePath = std::filesystem::weakly_canonical(document.directory_get() / spritesheet.path, ec); if (ec) absolutePath = document.directory_get() / spritesheet.path; - auto target = std::filesystem::is_directory(absolutePath) - ? absolutePath - : std::filesystem::is_directory(absolutePath.parent_path()) ? absolutePath.parent_path() - : document.directory_get(); + auto target = std::filesystem::is_directory(absolutePath) ? absolutePath + : std::filesystem::is_directory(absolutePath.parent_path()) ? absolutePath.parent_path() + : document.directory_get(); dialog.file_explorer_open(target); }; @@ -232,7 +231,7 @@ namespace anm2ed::imgui bool isValid = spritesheet.texture.is_valid(); auto& texture = isValid ? spritesheet.texture : resources.icons[icon::NONE]; auto tintColor = !isValid ? ImVec4(1.0f, 0.25f, 0.25f, 1.0f) : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); - auto pathString = filesystem::path_to_utf8(spritesheet.path); + auto pathString = path::to_utf8(spritesheet.path); auto pathCStr = pathString.c_str(); ImGui::SetNextItemSelectionUserData(id); @@ -322,12 +321,12 @@ namespace anm2ed::imgui } ImGui::EndChild(); - ImGui::PopID(); + + context_menu(); } ImGui::PopStyleVar(); - context_menu(); selection.finish(); } ImGui::EndChild(); @@ -339,7 +338,7 @@ namespace anm2ed::imgui if (ImGui::Button(localize.get(BASIC_ADD), rowOneWidgetSize)) add_open(); set_item_tooltip_shortcut(localize.get(TOOLTIP_ADD_SPRITESHEET), settings.shortcutAdd); - if (dialog.is_selected(dialog::SPRITESHEET_OPEN)) + if (dialog.is_selected(Dialog::SPRITESHEET_OPEN)) { add(dialog.path); dialog.reset(); @@ -359,7 +358,7 @@ namespace anm2ed::imgui ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPLACE_SPRITESHEET)); ImGui::EndDisabled(); - if (dialog.is_selected(dialog::SPRITESHEET_REPLACE)) + if (dialog.is_selected(Dialog::SPRITESHEET_REPLACE)) { replace(dialog.path); dialog.reset(); diff --git a/src/imgui/window/timeline.cpp b/src/imgui/window/timeline.cpp index be15a37..f2121f4 100644 --- a/src/imgui/window/timeline.cpp +++ b/src/imgui/window/timeline.cpp @@ -1069,7 +1069,7 @@ namespace anm2ed::imgui drawList->AddRectFilled(frameScreenPos, frameRectMax, ImGui::GetColorU32(frameMultipleOverlayColor)); } - frames.selection.start(item->frames.size(), ImGuiMultiSelectFlags_ClearOnEscape); + if (type != anm2::TRIGGER) frames.selection.start(item->frames.size(), ImGuiMultiSelectFlags_ClearOnEscape); for (auto [i, frame] : std::views::enumerate(item->frames)) { @@ -1400,7 +1400,8 @@ namespace anm2ed::imgui ImGui::PopID(); } - frames.selection.finish(); + if (type != anm2::TRIGGER) frames.selection.finish(); + if (isFrameSelectionLocked) { frames.selection.clear(); diff --git a/src/imgui/window/welcome.cpp b/src/imgui/window/welcome.cpp index 7fb1971..74a046d 100644 --- a/src/imgui/window/welcome.cpp +++ b/src/imgui/window/welcome.cpp @@ -2,11 +2,11 @@ #include -#include "filesystem_.h" +#include "path_.h" #include "strings.h" +using namespace anm2ed::util; using namespace anm2ed::resource; -namespace filesystem = anm2ed::util::filesystem; namespace anm2ed::imgui { @@ -34,10 +34,10 @@ namespace anm2ed::imgui auto widgetSize = widget_size_with_row_get(2); if (ImGui::Button(localize.get(BASIC_NEW), widgetSize)) - dialog.file_save(dialog::ANM2_NEW); // handled in taskbar.cpp + dialog.file_save(Dialog::ANM2_NEW); // handled in taskbar.cpp ImGui::SameLine(); if (ImGui::Button(localize.get(BASIC_OPEN), widgetSize)) - dialog.file_open(dialog::ANM2_OPEN); // handled in taskbar.cpp + dialog.file_open(Dialog::ANM2_OPEN); // handled in taskbar.cpp if (ImGui::BeginChild("##Recent Files Child", {}, ImGuiChildFlags_Borders)) { @@ -46,8 +46,7 @@ namespace anm2ed::imgui { ImGui::PushID(i); - auto label = std::format(FILE_LABEL_FORMAT, filesystem::path_to_utf8(file.filename()), - filesystem::path_to_utf8(file)); + auto label = std::format(FILE_LABEL_FORMAT, path::to_utf8(file.filename()), path::to_utf8(file)); if (ImGui::Selectable(label.c_str())) { @@ -78,8 +77,7 @@ namespace anm2ed::imgui { for (auto& file : manager.autosaveFiles) { - auto label = std::format(FILE_LABEL_FORMAT, filesystem::path_to_utf8(file.filename()), - filesystem::path_to_utf8(file)); + auto label = std::format(FILE_LABEL_FORMAT, path::to_utf8(file.filename()), path::to_utf8(file)); ImGui::TextUnformatted(label.c_str()); } } diff --git a/src/imgui/wizard/about.cpp b/src/imgui/wizard/about.cpp new file mode 100644 index 0000000..ea7fe6b --- /dev/null +++ b/src/imgui/wizard/about.cpp @@ -0,0 +1,191 @@ +#include "about.h" + +#include +#include +#include + +using namespace anm2ed::resource; + +namespace anm2ed::imgui::wizard +{ + static constexpr auto CREDIT_DELAY = 1.0f; + static constexpr auto CREDIT_SCROLL_SPEED = 25.0f; + + static constexpr About::Credit CREDITS[] = { + {"Anm2Ed", font::BOLD}, + {"License: GPLv3"}, + {""}, + {"Designer", font::BOLD}, + {"Shweet"}, + {""}, + {"Additional Help", font::BOLD}, + {"im-tem"}, + {""}, + {"Localization", font::BOLD}, + {"Gabriel Asencio (Spanish (Latin America))"}, + {"ExtremeThreat (Russian)"}, + {"CxRedix (Chinese)"}, + {"sawalk/사왈이 (Korean)"}, + {""}, + {"Based on the work of:", font::BOLD}, + {"Adrian Gavrilita"}, + {"Simon Parzer"}, + {"Matt Kapuszczak"}, + {""}, + {"XM Music", font::BOLD}, + {"Drozerix"}, + {"\"Keygen Wraith\""}, + {"https://modarchive.org/module.php?207854"}, + {"License: CC0"}, + {""}, + {"Libraries", font::BOLD}, + {"Dear ImGui"}, + {"https://github.com/ocornut/imgui"}, + {"License: MIT"}, + {""}, + {"SDL"}, + {"https://github.com/libsdl-org/SDL"}, + {"License: zlib"}, + {""}, + {"SDL_mixer"}, + {"https://github.com/libsdl-org/SDL_mixer"}, + {"License: zlib"}, + {""}, + {"tinyxml2"}, + {"https://github.com/leethomason/tinyxml2"}, + {"License: zlib"}, + {""}, + {"glm"}, + {"https://github.com/g-truc/glm"}, + {"License: MIT"}, + {""}, + {"lunasvg"}, + {"https://github.com/sammycage/lunasvg"}, + {"License: MIT"}, + {""}, + {"Icons", font::BOLD}, + {"Remix Icons"}, + {"remixicon.com"}, + {"License: Apache"}, + {""}, + {"Font", font::BOLD}, + {"Noto Sans"}, + {"https://fonts.google.com/noto/specimen/Noto+Sans"}, + {"License: OFL"}, + {""}, + {"Special Thanks", font::BOLD}, + {"Edmund McMillen"}, + {"Florian Himsl"}, + {"Tyrone Rodriguez"}, + {"The-Vinh Truong (_kilburn)"}, + {"Isaac Reflashed team"}, + {"Everyone who waited patiently for this to be finished"}, + {"Everyone else who has worked on The Binding of Isaac!"}, + {""}, + {""}, + {""}, + {""}, + {""}, + {""}, + {""}, + {""}, + {""}, + {"enjoy the jams :)"}, + {""}, + {""}, + {""}, + {""}, + {""}, + {""}, + {""}, + {""}, + {""}, + {""}, + }; + static constexpr auto CREDIT_COUNT = (int)(sizeof(CREDITS) / sizeof(About::Credit)); + + void About::reset(Resources& resources) + { + resources.music_track().play(true); + creditsState = {}; + creditsState.spawnTimer = CREDIT_DELAY; + } + + void About::update(Resources& resources) + { + auto size = ImGui::GetContentRegionAvail(); + auto applicationLabel = localize.get(LABEL_APPLICATION_NAME); + auto versionLabel = localize.get(LABEL_APPLICATION_VERSION); + + ImGui::PushFont(resources.fonts[font::BOLD].get(), font::SIZE_LARGE); + + ImGui::SetCursorPosX((size.x - ImGui::CalcTextSize(applicationLabel).x) / 2); + ImGui::TextUnformatted(applicationLabel); + + ImGui::SetCursorPosX((size.x - ImGui::CalcTextSize(versionLabel).x) / 2); + ImGui::TextUnformatted(versionLabel); + + ImGui::PopFont(); + + auto creditRegionPos = ImGui::GetCursorScreenPos(); + auto creditRegionSize = ImGui::GetContentRegionAvail(); + + if (creditRegionSize.y > 0.0f && creditRegionSize.x > 0.0f) + { + auto fontSize = ImGui::GetFontSize(); + auto drawList = ImGui::GetWindowDrawList(); + auto clipMax = ImVec2(creditRegionPos.x + creditRegionSize.x, creditRegionPos.y + creditRegionSize.y); + drawList->PushClipRect(creditRegionPos, clipMax, true); + + auto delta = ImGui::GetIO().DeltaTime; + creditsState.spawnTimer -= delta; + auto maxVisible = std::max(1, (int)std::floor(creditRegionSize.y / (float)fontSize)); + + while (creditsState.active.size() < (size_t)maxVisible && creditsState.spawnTimer <= 0.0f) + { + creditsState.active.push_back({creditsState.nextIndex, 0.0f}); + creditsState.nextIndex = (creditsState.nextIndex + 1) % CREDIT_COUNT; + creditsState.spawnTimer += CREDIT_DELAY; + } + + auto baseY = clipMax.y - (float)fontSize; + auto& baseColor = ImGui::GetStyleColorVec4(ImGuiCol_Text); + auto fadeSpan = (float)fontSize * 2.0f; + + for (auto it = creditsState.active.begin(); it != creditsState.active.end();) + { + it->offset += CREDIT_SCROLL_SPEED * delta; + auto yPos = baseY - it->offset; + if (yPos + fontSize < creditRegionPos.y) + { + it = creditsState.active.erase(it); + continue; + } + + auto& credit = CREDITS[it->index]; + auto fontPtr = resources.fonts[credit.font].get(); + auto textSize = fontPtr->CalcTextSizeA((float)fontSize, FLT_MAX, 0.0f, credit.string); + auto xPos = creditRegionPos.x + (creditRegionSize.x - textSize.x) * 0.5f; + + auto alpha = 1.0f; + auto topDist = yPos - creditRegionPos.y; + if (topDist < fadeSpan) alpha *= std::clamp(topDist / fadeSpan, 0.0f, 1.0f); + auto bottomDist = (creditRegionPos.y + creditRegionSize.y) - (yPos + fontSize); + if (bottomDist < fadeSpan) alpha *= std::clamp(bottomDist / fadeSpan, 0.0f, 1.0f); + if (alpha <= 0.0f) + { + ++it; + continue; + } + + auto color = baseColor; + color.w *= alpha; + + drawList->AddText(fontPtr, fontSize, ImVec2(xPos, yPos), ImGui::GetColorU32(color), credit.string); + ++it; + } + + drawList->PopClipRect(); + } + } +} \ No newline at end of file diff --git a/src/imgui/wizard/about.h b/src/imgui/wizard/about.h new file mode 100644 index 0000000..d1dcbcc --- /dev/null +++ b/src/imgui/wizard/about.h @@ -0,0 +1,36 @@ +#pragma once + +#include "../../resources.h" + +namespace anm2ed::imgui::wizard +{ + class About + { + public: + struct Credit + { + const char* string{}; + resource::font::Type font{resource::font::REGULAR}; + }; + + struct ScrollingCredit + { + int index{}; + float offset{}; + }; + + struct CreditsState + { + std::vector active{}; + float spawnTimer{1.0f}; + int nextIndex{}; + }; + + int creditsIndex{}; + CreditsState creditsState{}; + + void reset(Resources& resources); + void update(Resources& resources); + }; + +} \ No newline at end of file diff --git a/src/imgui/wizard/change_all_frame_properties.cpp b/src/imgui/wizard/change_all_frame_properties.cpp new file mode 100644 index 0000000..d85df38 --- /dev/null +++ b/src/imgui/wizard/change_all_frame_properties.cpp @@ -0,0 +1,270 @@ +#include "change_all_frame_properties.h" + +#include +#include +#include + +#include "math_.h" + +using namespace anm2ed::util::math; +using namespace glm; + +namespace anm2ed::imgui::wizard +{ + void ChangeAllFrameProperties::update(Document& document, Settings& settings) + { + isChanged = false; + + auto& frames = document.frames.selection; + auto& isCropX = settings.changeIsCropX; + auto& isCropY = settings.changeIsCropY; + auto& isSizeX = settings.changeIsSizeX; + auto& isSizeY = settings.changeIsSizeY; + auto& isPositionX = settings.changeIsPositionX; + auto& isPositionY = settings.changeIsPositionY; + auto& isPivotX = settings.changeIsPivotX; + auto& isPivotY = settings.changeIsPivotY; + auto& isScaleX = settings.changeIsScaleX; + auto& isScaleY = settings.changeIsScaleY; + auto& isRotation = settings.changeIsRotation; + auto& isDuration = settings.changeIsDuration; + auto& isTintR = settings.changeIsTintR; + auto& isTintG = settings.changeIsTintG; + auto& isTintB = settings.changeIsTintB; + auto& isTintA = settings.changeIsTintA; + auto& isColorOffsetR = settings.changeIsColorOffsetR; + auto& isColorOffsetG = settings.changeIsColorOffsetG; + auto& isColorOffsetB = settings.changeIsColorOffsetB; + auto& isVisibleSet = settings.changeIsVisibleSet; + auto& isInterpolatedSet = settings.changeIsInterpolatedSet; + auto& crop = settings.changeCrop; + auto& size = settings.changeSize; + auto& position = settings.changePosition; + auto& pivot = settings.changePivot; + auto& scale = settings.changeScale; + auto& rotation = settings.changeRotation; + auto& duration = settings.changeDuration; + auto& tint = settings.changeTint; + auto& colorOffset = settings.changeColorOffset; + auto& isVisible = settings.changeIsVisible; + auto& isInterpolated = settings.changeIsInterpolated; + +#define PROPERTIES_WIDGET(body, checkboxLabel, isEnabled) \ + ImGui::Checkbox(checkboxLabel, &isEnabled); \ + ImGui::SameLine(); \ + ImGui::BeginDisabled(!isEnabled); \ + body; \ + ImGui::EndDisabled(); + + auto bool_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, bool& value) + { PROPERTIES_WIDGET(ImGui::Checkbox(valueLabel, &value), checkboxLabel, isEnabled) }; + + auto color3_value = [&](const char* checkboxRLabel, const char* checkboxGLabel, const char* checkboxBLabel, + const char* valueRLabel, const char* valueGLabel, const char* valueBLabel, + const char* label, bool& isREnabled, bool& isGEnabled, bool& isBEnabled, vec3& value) + { + auto style = ImGui::GetStyle(); + + auto width = (ImGui::CalcItemWidth() - (ImGui::GetFrameHeightWithSpacing() * 2) - (style.ItemSpacing.x * 2) - + ImGui::GetFrameHeight()) / + 3; + + ivec3 valueAlt = {float_to_uint8(value.r), float_to_uint8(value.g), float_to_uint8(value.b)}; + + ImGui::PushItemWidth(width); + + PROPERTIES_WIDGET(ImGui::DragInt(valueRLabel, &valueAlt.r, DRAG_SPEED, 0, 255, "R:%d"), checkboxRLabel, + isREnabled); + + ImGui::SameLine(); + + PROPERTIES_WIDGET(ImGui::DragInt(valueGLabel, &valueAlt.g, DRAG_SPEED, 0, 255, "G:%d"), checkboxGLabel, + isGEnabled); + + ImGui::SameLine(); + + PROPERTIES_WIDGET(ImGui::DragInt(valueBLabel, &valueAlt.b, DRAG_SPEED, 0, 255, "B:%d"), checkboxBLabel, + isBEnabled); + + ImGui::PopItemWidth(); + + ImGui::SameLine(); + + value = vec3(uint8_to_float(valueAlt.r), uint8_to_float(valueAlt.g), uint8_to_float(valueAlt.b)); + + ImVec4 buttonColor = {isREnabled ? value.r : 0, isGEnabled ? value.g : 0, isBEnabled ? value.b : 0, 1}; + + ImGui::ColorButton(label, buttonColor); + + ImGui::SameLine(); + + ImGui::TextUnformatted(label); + }; + + auto color4_value = [&](const char* checkboxRLabel, const char* checkboxGLabel, const char* checkboxBLabel, + const char* checkboxALabel, const char* valueRLabel, const char* valueGLabel, + const char* valueBLabel, const char* valueALabel, const char* label, bool& isREnabled, + bool& isGEnabled, bool& isBEnabled, bool& isAEnabled, vec4& value) + { + auto style = ImGui::GetStyle(); + auto width = (ImGui::CalcItemWidth() - (ImGui::GetFrameHeightWithSpacing() * 3) - (style.ItemSpacing.x * 3) - + ImGui::GetFrameHeight()) / + 4; + ivec4 valueAlt = {float_to_uint8(value.r), float_to_uint8(value.g), float_to_uint8(value.b), + float_to_uint8(value.a)}; + + ImGui::PushItemWidth(width); + + PROPERTIES_WIDGET(ImGui::DragInt(valueRLabel, &valueAlt.r, DRAG_SPEED, 0, 255, "R:%d"), checkboxRLabel, + isREnabled); + ImGui::SameLine(); + + PROPERTIES_WIDGET(ImGui::DragInt(valueGLabel, &valueAlt.g, DRAG_SPEED, 0, 255, "G:%d"), checkboxGLabel, + isGEnabled); + + ImGui::SameLine(); + + PROPERTIES_WIDGET(ImGui::DragInt(valueBLabel, &valueAlt.b, DRAG_SPEED, 0, 255, "B:%d"), checkboxBLabel, + isBEnabled); + + ImGui::SameLine(); + + PROPERTIES_WIDGET(ImGui::DragInt(valueALabel, &valueAlt.a, DRAG_SPEED, 0, 255, "A:%d"), checkboxALabel, + isAEnabled); + + ImGui::PopItemWidth(); + + ImGui::SameLine(); + + value = vec4(uint8_to_float(valueAlt.r), uint8_to_float(valueAlt.g), uint8_to_float(valueAlt.b), + uint8_to_float(valueAlt.a)); + + ImVec4 buttonColor = {isREnabled ? value.r : 0, isGEnabled ? value.g : 0, isBEnabled ? value.b : 0, + isAEnabled ? value.a : 1}; + ImGui::ColorButton(label, buttonColor); + + ImGui::SameLine(); + + ImGui::TextUnformatted(label); + }; + + auto float_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, float& value) + { + PROPERTIES_WIDGET(ImGui::DragFloat(valueLabel, &value, DRAG_SPEED, 0.0f, 0.0f, float_format_get(value)), + checkboxLabel, isEnabled); + }; + + auto float2_value = [&](const char* checkboxXLabel, const char* checkboxYLabel, const char* valueXLabel, + const char* valueYLabel, bool& isXEnabled, bool& isYEnabled, vec2& value) + { + auto style = ImGui::GetStyle(); + + auto width = (ImGui::CalcItemWidth() - ImGui::GetFrameHeightWithSpacing() - style.ItemSpacing.x) / 2; + + ImGui::PushItemWidth(width); + PROPERTIES_WIDGET(ImGui::DragFloat(valueXLabel, &value.x, DRAG_SPEED, 0.0f, 0.0f, float_format_get(value.x)), + checkboxXLabel, isXEnabled); + ImGui::SameLine(); + PROPERTIES_WIDGET(ImGui::DragFloat(valueYLabel, &value.y, DRAG_SPEED, 0.0f, 0.0f, float_format_get(value.y)), + checkboxYLabel, isYEnabled); + ImGui::PopItemWidth(); + }; + + auto duration_value = [&](const char* checkboxLabel, const char* valueLabel, bool& isEnabled, int& value) + { + PROPERTIES_WIDGET( + input_int_range(valueLabel, value, anm2::FRAME_DURATION_MIN, anm2::FRAME_DURATION_MAX, STEP, STEP_FAST), + checkboxLabel, isEnabled); + }; + +#undef PROPERTIES_WIDGET + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImGui::GetStyle().ItemInnerSpacing); + + float2_value("##Is Crop X", "##Is Crop Y", "##Crop X", localize.get(BASIC_CROP), isCropX, isCropY, crop); + float2_value("##Is Size X", "##Is Size Y", "##Size X", localize.get(BASIC_SIZE), isSizeX, isSizeY, size); + float2_value("##Is Position X", "##Is Position Y", "##Position X", localize.get(BASIC_POSITION), isPositionX, + isPositionY, position); + float2_value("##Is Pivot X", "##Is Pivot Y", "##Pivot X", localize.get(BASIC_PIVOT), isPivotX, isPivotY, pivot); + float2_value("##Is Scale X", "##Is Scale Y", "##Scale X", localize.get(BASIC_SCALE), isScaleX, isScaleY, scale); + float_value("##Is Rotation", localize.get(BASIC_ROTATION), isRotation, rotation); + duration_value("##Is Duration", localize.get(BASIC_DURATION), isDuration, duration); + color4_value("##Is Tint R", "##Is Tint G", "##Is Tint B", "##Is Tint A", "##Tint R", "##Tint G", "##Tint B", + "##Tint A", localize.get(BASIC_TINT), isTintR, isTintG, isTintB, isTintA, tint); + color3_value("##Is Color Offset R", "##Is Color Offset G", "##Is Color Offset B", "##Color Offset R", + "##Color Offset B", "##Color Offset G", localize.get(BASIC_COLOR_OFFSET), isColorOffsetR, + isColorOffsetG, isColorOffsetB, colorOffset); + bool_value("##Is Visible", localize.get(BASIC_VISIBLE), isVisibleSet, isVisible); + ImGui::SameLine(); + bool_value("##Is Interpolated", localize.get(BASIC_INTERPOLATED), isInterpolatedSet, isInterpolated); + + ImGui::PopStyleVar(); + + auto frame_change = [&](anm2::ChangeType type) + { + anm2::FrameChange frameChange; + if (isCropX) frameChange.cropX = crop.x; + if (isCropY) frameChange.cropY = crop.y; + if (isSizeX) frameChange.sizeX = size.x; + if (isSizeY) frameChange.sizeY = size.y; + if (isPositionX) frameChange.positionX = position.x; + if (isPositionY) frameChange.positionY = position.y; + if (isPivotX) frameChange.pivotX = pivot.x; + if (isPivotY) frameChange.pivotY = pivot.y; + if (isScaleX) frameChange.scaleX = scale.x; + if (isScaleY) frameChange.scaleY = scale.y; + if (isRotation) frameChange.rotation = std::make_optional(rotation); + if (isDuration) frameChange.duration = std::make_optional(duration); + if (isTintR) frameChange.tintR = tint.r; + if (isTintG) frameChange.tintG = tint.g; + if (isTintB) frameChange.tintB = tint.b; + if (isTintA) frameChange.tintA = tint.a; + if (isColorOffsetR) frameChange.colorOffsetR = colorOffset.r; + if (isColorOffsetG) frameChange.colorOffsetG = colorOffset.g; + if (isColorOffsetB) frameChange.colorOffsetB = colorOffset.b; + if (isVisibleSet) frameChange.isVisible = std::make_optional(isVisible); + if (isInterpolatedSet) frameChange.isInterpolated = std::make_optional(isInterpolated); + + DOCUMENT_EDIT(document, localize.get(EDIT_CHANGE_FRAME_PROPERTIES), Document::FRAMES, + document.item_get()->frames_change(frameChange, type, *frames.begin(), (int)frames.size())); + + isChanged = true; + }; + + ImGui::Separator(); + + bool isAnyProperty = isCropX || isCropY || isSizeX || isSizeY || isPositionX || isPositionY || isPivotX || + isPivotY || isScaleX || isScaleY || isRotation || isDuration || isTintR || isTintG || + isTintB || isTintA || isColorOffsetR || isColorOffsetG || isColorOffsetB || isVisibleSet || + isInterpolatedSet; + + auto rowWidgetSize = widget_size_with_row_get(5); + + ImGui::BeginDisabled(!isAnyProperty); + + if (ImGui::Button(localize.get(LABEL_ADJUST), rowWidgetSize)) frame_change(anm2::ADJUST); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADJUST)); + + ImGui::SameLine(); + + if (ImGui::Button(localize.get(BASIC_ADD), rowWidgetSize)) frame_change(anm2::ADD); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ADD_VALUES)); + + ImGui::SameLine(); + + if (ImGui::Button(localize.get(LABEL_SUBTRACT), rowWidgetSize)) frame_change(anm2::SUBTRACT); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SUBTRACT_VALUES)); + + ImGui::SameLine(); + + if (ImGui::Button(localize.get(LABEL_MULTIPLY), rowWidgetSize)) frame_change(anm2::MULTIPLY); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MULTIPLY_VALUES)); + + ImGui::SameLine(); + + if (ImGui::Button(localize.get(LABEL_DIVIDE), rowWidgetSize)) frame_change(anm2::DIVIDE); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_DIVIDE_VALUES)); + + ImGui::EndDisabled(); + } +} diff --git a/src/imgui/wizard/change_all_frame_properties.h b/src/imgui/wizard/change_all_frame_properties.h new file mode 100644 index 0000000..f5143ee --- /dev/null +++ b/src/imgui/wizard/change_all_frame_properties.h @@ -0,0 +1,15 @@ +#pragma once + +#include "document.h" +#include "settings.h" + +namespace anm2ed::imgui::wizard +{ + class ChangeAllFrameProperties + { + public: + bool isChanged{}; + + void update(Document&, Settings&); + }; +} diff --git a/src/imgui/wizard/configure.cpp b/src/imgui/wizard/configure.cpp new file mode 100644 index 0000000..e40df47 --- /dev/null +++ b/src/imgui/wizard/configure.cpp @@ -0,0 +1,201 @@ +#include "configure.h" + +#include "imgui_.h" + +using namespace anm2ed::types; + +namespace anm2ed::imgui::wizard +{ + void Configure::reset(Settings& settings) { temporary = settings; } + + void Configure::update(Manager& manager, Settings& settings) + { + isSet = false; + + auto childSize = size_without_footer_get(2); + + if (ImGui::BeginTabBar("##Configure Tabs")) + { + if (ImGui::BeginTabItem(localize.get(LABEL_DISPLAY))) + { + if (ImGui::BeginChild("##Tab Child", childSize, true)) + { + ImGui::SeparatorText(localize.get(LABEL_WINDOW_MENU)); + input_float_range(localize.get(LABEL_UI_SCALE), temporary.uiScale, 0.5f, 2.0f, 0.25f, 0.25f, "%.2f"); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_UI_SCALE)); + ImGui::Checkbox(localize.get(LABEL_VSYNC), &temporary.isVsync); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_VSYNC)); + + ImGui::SeparatorText(localize.get(LABEL_LOCALIZATION)); + ImGui::Combo(localize.get(LABEL_LANGUAGE), &temporary.language, LANGUAGE_STRINGS, LANGUAGE_COUNT); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_LANGUAGE)); + + ImGui::SeparatorText(localize.get(LABEL_THEME)); + + for (int i = 0; i < theme::COUNT; i++) + { + ImGui::RadioButton(localize.get(theme::STRINGS[i]), &temporary.theme, i); + ImGui::SameLine(); + } + } + ImGui::EndChild(); + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem(localize.get(LABEL_FILE_MENU))) + { + if (ImGui::BeginChild("##Tab Child", childSize, true)) + { + ImGui::SeparatorText(localize.get(LABEL_AUTOSAVE)); + + ImGui::Checkbox(localize.get(BASIC_ENABLED), &temporary.fileIsAutosave); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_AUTOSAVE_ENABLED)); + + ImGui::BeginDisabled(!temporary.fileIsAutosave); + input_int_range(localize.get(LABEL_TIME_MINUTES), temporary.fileAutosaveTime, 0, 10); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_AUTOSAVE_INTERVAL)); + ImGui::EndDisabled(); + + ImGui::SeparatorText(localize.get(LABEL_SNAPSHOTS)); + input_int_range(localize.get(LABEL_STACK_SIZE), temporary.fileSnapshotStackSize, 0, 100); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_STACK_SIZE)); + + ImGui::SeparatorText(localize.get(LABEL_OPTIONS)); + ImGui::Checkbox(localize.get(LABEL_OVERWRITE_WARNING), &temporary.fileIsWarnOverwrite); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OVERWRITE_WARNING)); + } + ImGui::EndChild(); + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem(localize.get(LABEL_INPUT))) + { + if (ImGui::BeginChild("##Tab Child", childSize, true)) + { + ImGui::SeparatorText(localize.get(LABEL_KEYBOARD)); + + input_float_range(localize.get(LABEL_REPEAT_DELAY), temporary.keyboardRepeatDelay, 0.05f, 1.0f, 0.05f, 0.05f, + "%.2f"); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPEAT_DELAY)); + + input_float_range(localize.get(LABEL_REPEAT_RATE), temporary.keyboardRepeatRate, 0.005f, 1.0f, 0.005f, 0.005f, + "%.3f"); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_REPEAT_DELAY)); + + ImGui::SeparatorText(localize.get(LABEL_ZOOM)); + + input_float_range(localize.get(LABEL_ZOOM_STEP), temporary.inputZoomStep, 10.0f, 250.0f, 10.0f, 10.0f, + "%.0f%%"); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ZOOM_STEP)); + + ImGui::SeparatorText(localize.get(LABEL_TOOL)); + + ImGui::Checkbox(localize.get(LABEL_MOVE_TOOL_SNAP), &temporary.inputIsMoveToolSnapToMouse); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_MOVE_TOOL_SNAP)); + } + ImGui::EndChild(); + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem(localize.get(LABEL_SHORTCUTS_TAB))) + { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2()); + + if (ImGui::BeginChild("##Tab Child", childSize, true)) + { + if (ImGui::BeginTable(localize.get(LABEL_SHORTCUTS_TAB), 2, + ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY)) + { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn(localize.get(LABEL_SHORTCUT_COLUMN)); + ImGui::TableSetupColumn(localize.get(LABEL_VALUE_COLUMN)); + ImGui::TableHeadersRow(); + + for (int i = 0; i < SHORTCUT_COUNT; ++i) + { + bool isSelected = selectedShortcut == i; + + ShortcutMember member = SHORTCUT_MEMBERS[i]; + std::string* settingString = &(temporary.*member); + std::string chordString = isSelected ? "" : *settingString; + + ImGui::PushID(i); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(localize.get(::anm2ed::SHORTCUT_STRING_TYPES[i])); + ImGui::TableSetColumnIndex(1); + + if (ImGui::Selectable(chordString.c_str(), isSelected)) selectedShortcut = i; + + ImGui::PopID(); + + if (isSelected) + { + ImGuiKeyChord chord{ImGuiKey_None}; + + if (ImGui::IsKeyDown(ImGuiMod_Ctrl)) chord |= ImGuiMod_Ctrl; + if (ImGui::IsKeyDown(ImGuiMod_Shift)) chord |= ImGuiMod_Shift; + if (ImGui::IsKeyDown(ImGuiMod_Alt)) chord |= ImGuiMod_Alt; + if (ImGui::IsKeyDown(ImGuiMod_Super)) chord |= ImGuiMod_Super; + + for (const auto& entry : KEY_MAP) + { + auto key = entry.second; + if (ImGui::IsKeyPressed(key)) + { + chord |= key; + *settingString = chord_to_string(chord); + selectedShortcut = -1; + break; + } + } + } + } + ImGui::EndTable(); + } + ImGui::EndChild(); + + ImGui::PopStyleVar(); + + ImGui::EndTabItem(); + } + } + + ImGui::EndTabBar(); + } + + auto widgetSize = widget_size_with_row_get(3); + + if (ImGui::Button(localize.get(BASIC_SAVE), widgetSize)) + { + settings = temporary; + + ImGui::GetIO().KeyRepeatDelay = settings.keyboardRepeatDelay; + ImGui::GetIO().KeyRepeatRate = settings.keyboardRepeatRate; + ImGui::GetStyle().FontScaleMain = settings.uiScale; + SnapshotStack::max_size_set(settings.fileSnapshotStackSize); + imgui::theme_set((theme::Type)settings.theme); + localize.language = (Language)settings.language; + manager.chords_set(settings); + + for (auto& document : manager.documents) + document.snapshots.apply_limit(); + + isSet = true; + } + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SETTINGS_SAVE)); + + ImGui::SameLine(); + + if (ImGui::Button(localize.get(LABEL_USE_DEFAULT_SETTINGS), widgetSize)) temporary = Settings(); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_USE_DEFAULT_SETTINGS)); + + ImGui::SameLine(); + + if (ImGui::Button(localize.get(LABEL_CLOSE), widgetSize)) isSet = true; + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CLOSE_SETTINGS)); + } +} \ No newline at end of file diff --git a/src/imgui/wizard/configure.h b/src/imgui/wizard/configure.h new file mode 100644 index 0000000..98840a5 --- /dev/null +++ b/src/imgui/wizard/configure.h @@ -0,0 +1,18 @@ +#pragma once + +#include "manager.h" + +namespace anm2ed::imgui::wizard +{ + class Configure + { + Settings temporary{}; + int selectedShortcut{-1}; + + public: + bool isSet{}; + + void reset(Settings&); + void update(Manager&, Settings&); + }; +} \ No newline at end of file diff --git a/src/imgui/wizard/generate_animation_from_grid.cpp b/src/imgui/wizard/generate_animation_from_grid.cpp new file mode 100644 index 0000000..97083c6 --- /dev/null +++ b/src/imgui/wizard/generate_animation_from_grid.cpp @@ -0,0 +1,115 @@ +#include "generate_animation_from_grid.h" + +#include "math_.h" +#include "types.h" + +using namespace anm2ed::types; +using namespace anm2ed::util; +using namespace glm; + +namespace anm2ed::imgui::wizard +{ + GenerateAnimationFromGrid::GenerateAnimationFromGrid() : Canvas(vec2()) {} + + void GenerateAnimationFromGrid::update(Document& document, Resources& resources, Settings& settings) + { + isEnd = false; + + auto& startPosition = settings.generateStartPosition; + auto& size = settings.generateSize; + auto& pivot = settings.generatePivot; + auto& rows = settings.generateRows; + auto& columns = settings.generateColumns; + auto& count = settings.generateCount; + auto& delay = settings.generateDuration; + auto& zoom = settings.generateZoom; + auto& zoomStep = settings.inputZoomStep; + + auto childSize = ImVec2(row_widget_width_get(2), size_without_footer_get().y); + + if (ImGui::BeginChild("##Options Child", childSize, ImGuiChildFlags_Borders)) + { + ImGui::InputInt2(localize.get(LABEL_GENERATE_START_POSITION), value_ptr(startPosition)); + ImGui::InputInt2(localize.get(LABEL_GENERATE_FRAME_SIZE), value_ptr(size)); + ImGui::InputInt2(localize.get(BASIC_PIVOT), value_ptr(pivot)); + ImGui::InputInt(localize.get(LABEL_GENERATE_ROWS), &rows, STEP, STEP_FAST); + ImGui::InputInt(localize.get(LABEL_GENERATE_COLUMNS), &columns, STEP, STEP_FAST); + + input_int_range(localize.get(LABEL_GENERATE_COUNT), count, anm2::FRAME_NUM_MIN, rows * columns); + + ImGui::InputInt(localize.get(BASIC_DURATION), &delay, STEP, STEP_FAST); + } + ImGui::EndChild(); + + ImGui::SameLine(); + + if (ImGui::BeginChild("##Preview Child", childSize, ImGuiChildFlags_Borders)) + { + auto& backgroundColor = settings.previewBackgroundColor; + auto& shaderTexture = resources.shaders[resource::shader::TEXTURE]; + + auto previewSize = ImVec2(ImGui::GetContentRegionAvail().x, size_without_footer_get(2).y); + + bind(); + size_set(to_vec2(previewSize)); + viewport_set(); + clear(vec4(backgroundColor, 1.0f)); + + if (document.reference.itemType == anm2::LAYER) + { + auto& texture = + document.anm2.content.spritesheets[document.anm2.content.layers[document.reference.itemID].spritesheetID] + .texture; + + auto index = std::clamp((int)(time * (count - 1)), 0, (count - 1)); + auto row = index / columns; + auto column = index % columns; + auto crop = startPosition + ivec2(size.x * column, size.y * row); + auto uvMin = (vec2(crop) + vec2(0.5f)) / vec2(texture.size); + auto uvMax = (vec2(crop) + vec2(size) - vec2(0.5f)) / vec2(texture.size); + + mat4 transform = transform_get(zoom) * math::quad_model_get(size, {}, pivot); + + texture_render(shaderTexture, texture.id, transform, vec4(1.0f), {}, + math::uv_vertices_get(uvMin, uvMax).data()); + } + + unbind(); + + ImGui::Image(texture, previewSize); + + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::SliderFloat("##Time", &time, 0.0f, 1.0f, ""); + + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputFloat("##Zoom", &zoom, zoomStep, zoomStep, "%.0f%%"); + zoom = glm::clamp(zoom, ZOOM_MIN, ZOOM_MAX); + } + + ImGui::EndChild(); + + auto widgetSize = widget_size_with_row_get(2); + + if (ImGui::Button(localize.get(LABEL_GENERATE), widgetSize)) + { + auto generate_from_grid = [&]() + { + auto item = document.item_get(); + auto animation = document.animation_get(); + + if (item && animation) + { + item->frames_generate_from_grid(startPosition, size, pivot, columns, count, delay); + animation->frameNum = animation->length(); + } + }; + + DOCUMENT_EDIT(document, localize.get(EDIT_GENERATE_ANIMATION_FROM_GRID), Document::FRAMES, generate_from_grid()); + isEnd = true; + } + + ImGui::SameLine(); + + if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) isEnd = true; + } +} \ No newline at end of file diff --git a/src/imgui/wizard/generate_animation_from_grid.h b/src/imgui/wizard/generate_animation_from_grid.h new file mode 100644 index 0000000..ca398bf --- /dev/null +++ b/src/imgui/wizard/generate_animation_from_grid.h @@ -0,0 +1,20 @@ +#pragma once + +#include "canvas.h" +#include "document.h" +#include "resources.h" +#include "settings.h" + +namespace anm2ed::imgui::wizard +{ + class GenerateAnimationFromGrid : public Canvas + { + float time{}; + + public: + bool isEnd{}; + + GenerateAnimationFromGrid(); + void update(Document&, Resources&, Settings&); + }; +} diff --git a/src/imgui/wizard/render_animation.cpp b/src/imgui/wizard/render_animation.cpp new file mode 100644 index 0000000..f441b57 --- /dev/null +++ b/src/imgui/wizard/render_animation.cpp @@ -0,0 +1,290 @@ +#include "render_animation.h" + +#include + +#include "log.h" +#include "path_.h" +#include "process_.h" +#include "toast.h" + +#include + +using namespace anm2ed::resource; +using namespace anm2ed::util; + +namespace anm2ed::imgui::wizard +{ + void RenderAnimation::range_to_animation_set(Manager& manager, Document& document) + { + if (auto animation = document.animation_get()) + { + manager.recordingStart = 0; + manager.recordingEnd = animation->frameNum - 1; + } + } + + void RenderAnimation::range_to_frames_set(Manager& manager, Document& document) + { + auto& frames = document.frames.selection; + if (!frames.empty()) + { + if (auto item = document.item_get()) + { + int duration{}; + for (auto [i, frame] : std::views::enumerate(item->frames)) + { + if ((int)i == *frames.begin()) manager.recordingStart = duration; + if ((int)i == *frames.rbegin()) manager.recordingEnd = duration + frame.duration - 1; + + duration += frame.duration; + } + } + } + } + + void RenderAnimation::reset(Manager& manager, Document& document, Settings& settings) + { + if (!manager.isRecordingRange) range_to_animation_set(manager, document); + settings.renderPath.replace_extension(render::EXTENSIONS[settings.renderType]); + } + + void RenderAnimation::update(Manager& manager, Document& document, Resources& resources, Settings& settings, + Dialog& dialog) + { + isEnd = false; + + auto animation = document.animation_get(); + if (!animation) return; + + auto& ffmpegPath = settings.renderFFmpegPath; + auto& path = settings.renderPath; + auto& format = settings.renderFormat; + auto& scale = settings.renderScale; + auto& isRaw = settings.renderIsRawAnimation; + auto& type = settings.renderType; + auto& start = manager.recordingStart; + auto& end = manager.recordingEnd; + auto& rows = settings.renderRows; + auto& columns = settings.renderColumns; + auto& isRange = manager.isRecordingRange; + auto& frames = document.frames.selection; + auto& reference = document.reference; + auto& frameNum = animation->frameNum; + + auto widgetSize = widget_size_with_row_get(2); + auto dialogType = type == render::PNGS ? Dialog::PNG_DIRECTORY_SET + : type == render::SPRITESHEET ? Dialog::PNG_PATH_SET + : type == render::GIF ? Dialog::GIF_PATH_SET + : type == render::WEBM ? Dialog::WEBM_PATH_SET + : Dialog::NONE; + + if (ImGui::ImageButton("##FFmpeg Path Set", resources.icons[icon::FOLDER].id, icon_size_get())) + dialog.file_open(Dialog::FFMPEG_PATH_SET); + ImGui::SameLine(); + input_text_path(localize.get(LABEL_FFMPEG_PATH), &ffmpegPath); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FFMPEG_PATH)); + + if (dialog.is_selected(Dialog::FFMPEG_PATH_SET)) + { + ffmpegPath = path::to_utf8(dialog.path); + dialog.reset(); + } + + if (ImGui::ImageButton("##Path Set", resources.icons[icon::FOLDER].id, icon_size_get())) + { + if (dialogType == Dialog::PNG_DIRECTORY_SET) + dialog.folder_open(dialogType); + else + dialog.file_save(dialogType); + } + ImGui::SameLine(); + auto pathLabel = type == render::PNGS ? LABEL_OUTPUT_DIRECTORY : LABEL_OUTPUT_PATH; + input_text_path(localize.get(pathLabel), &path); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_OUTPUT_PATH)); + + if (dialog.is_selected(dialogType)) + { + path = path::to_utf8(dialog.path); + dialog.reset(); + } + + if (ImGui::Combo(localize.get(LABEL_TYPE), &type, render::STRINGS, render::COUNT)) + path.replace_extension(render::EXTENSIONS[type]); + + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RENDER_TYPE)); + + if (type == render::PNGS || type == render::SPRITESHEET) ImGui::Separator(); + + if (type == render::PNGS) + { + input_text_path(localize.get(LABEL_FORMAT), &format); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_FORMAT)); + } + else if (type == render::SPRITESHEET) + { + input_int_range(localize.get(LABEL_GENERATE_ROWS), rows, 1, frameNum - 1); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_ROWS)); + + input_int_range(localize.get(LABEL_GENERATE_COLUMNS), columns, 1, frameNum - 1); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_COLUMNS)); + } + + ImGui::Separator(); + + ImGui::Checkbox(localize.get(LABEL_CUSTOM_RANGE), &isRange); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_CUSTOM_RANGE)); + + ImGui::SameLine(); + + ImGui::BeginDisabled(frames.empty() || reference.itemID == anm2::TRIGGER); + if (ImGui::Button(localize.get(LABEL_TO_SELECTED_FRAMES))) range_to_frames_set(manager, document); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TO_SELECTED_FRAMES)); + ImGui::EndDisabled(); + + ImGui::SameLine(); + + if (ImGui::Button(localize.get(LABEL_TO_ANIMATION_RANGE))) range_to_animation_set(manager, document); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_TO_ANIMATION_RANGE)); + + ImGui::BeginDisabled(!isRange); + { + input_int_range(localize.get(LABEL_START), start, 0, frameNum - 1); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_START)); + input_int_range(localize.get(LABEL_END), end, start, frameNum - 1); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_END)); + } + ImGui::EndDisabled(); + + ImGui::Separator(); + + ImGui::Checkbox(localize.get(LABEL_RAW), &isRaw); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RAW)); + + ImGui::BeginDisabled(!isRaw); + { + input_float_range(localize.get(BASIC_SCALE), scale, 1.0f, 100.0f, STEP, STEP_FAST, "%.1fx"); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SCALE_OUTPUT)); + } + ImGui::EndDisabled(); + + ImGui::Separator(); + + ImGui::Checkbox(localize.get(LABEL_SOUND), &settings.timelineIsSound); + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_SOUND)); + + ImGui::Separator(); + + if (ImGui::Button(localize.get(LABEL_RENDER), widgetSize)) + { + path.replace_extension(render::EXTENSIONS[type]); + + auto render_is_valid = [&]() + { + auto ffmpeg_is_valid = [&]() + { + if (!path::is_executable(ffmpegPath)) return false; + + auto testCommand = ffmpegPath.string() + " -version"; + Process process(testCommand.c_str(), "r"); + auto result = process.output_get_and_close(); + + if (!result.contains("ffmpeg version")) return false; + + return true; + }; + + auto ffmpeg_valid_check = [&]() + { + if (!ffmpeg_is_valid()) + { + toasts.push(localize.get(TOAST_INVALID_FFMPEG)); + logger.error(localize.get(TOAST_INVALID_FFMPEG, anm2ed::ENGLISH)); + return false; + } + return true; + }; + + auto png_format_valid_check = [&]() + { + if (!format.string().contains("{}")) + { + toasts.push(localize.get(TOAST_PNG_FORMAT_INVALID)); + logger.error(localize.get(TOAST_PNG_FORMAT_INVALID, anm2ed::ENGLISH)); + } + return true; + }; + + auto path_valid_check = [&]() + { + if (path.empty()) + { + toasts.push(localize.get(TOAST_RENDER_PATH_EMPTY)); + logger.error(localize.get(TOAST_RENDER_PATH_EMPTY, anm2ed::ENGLISH)); + return false; + } + return true; + }; + + auto png_directory_valid_check = [&]() + { + if (!path::ensure_directory(path)) + { + toasts.push(localize.get(TOAST_PNG_DIRECTORY_INVALID)); + logger.error(localize.get(TOAST_PNG_DIRECTORY_INVALID, anm2ed::ENGLISH)); + return false; + } + return true; + }; + + auto spritesheet_valid_check = [&]() + { + if (rows <= 0 && columns <= 0) + { + toasts.push(localize.get(TOAST_RENDER_PATH_EMPTY)); + logger.error(localize.get(TOAST_RENDER_PATH_EMPTY, anm2ed::ENGLISH)); + return false; + } + return true; + }; + + if (!path_valid_check()) return false; + + switch (type) + { + case render::PNGS: + if (!png_directory_valid_check()) return false; + if (!png_format_valid_check()) return false; + format.replace_extension(render::EXTENSIONS[render::SPRITESHEET]); + break; + case render::SPRITESHEET: + if (!spritesheet_valid_check()) return false; + break; + case render::GIF: + case render::WEBM: + case render::MP4: + if (!ffmpeg_valid_check()) return false; + break; + default: + return false; + break; + } + + return true; + }; + + if (render_is_valid()) + { + manager.isRecordingStart = true; + manager.progressPopup.open(); + } + + isEnd = true; + } + + ImGui::SetItemTooltip("%s", localize.get(TOOLTIP_RENDER_BUTTON)); + + ImGui::SameLine(); + + if (ImGui::Button(localize.get(BASIC_CANCEL), widgetSize)) isEnd = true; + } +} \ No newline at end of file diff --git a/src/imgui/wizard/render_animation.h b/src/imgui/wizard/render_animation.h new file mode 100644 index 0000000..77f20da --- /dev/null +++ b/src/imgui/wizard/render_animation.h @@ -0,0 +1,20 @@ +#pragma once + +#include "dialog.h" +#include "manager.h" +#include "resources.h" + +namespace anm2ed::imgui::wizard +{ + class RenderAnimation + { + + public: + bool isEnd{}; + + void range_to_frames_set(Manager&, Document&); + void range_to_animation_set(Manager&, Document&); + void reset(Manager&, Document&, Settings&); + void update(Manager&, Document&, Resources&, Settings&, Dialog&); + }; +} diff --git a/src/loader.cpp b/src/loader.cpp index 777b9b7..6de9a9c 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -8,8 +8,8 @@ #include -#include "filesystem_.h" #include "log.h" +#include "sdl.h" #include "imgui_.h" @@ -75,7 +75,7 @@ namespace anm2ed } } - std::filesystem::path Loader::settings_path() { return filesystem::path_preferences_get() / "settings.ini"; } + std::filesystem::path Loader::settings_path() { return sdl::preferences_directory_get() / "settings.ini"; } Loader::Loader(int argc, const char** argv) { diff --git a/src/log.cpp b/src/log.cpp index 25bac06..88bcd80 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -2,53 +2,37 @@ #include -#include "filesystem_.h" +#include "sdl.h" #include "time_.h" using namespace anm2ed::util; namespace anm2ed { + void Logger::write_raw(const std::string& message) + { + std::println("{}", message); + if (file.is_open()) file << message << '\n' << std::flush; + } + void Logger::write(const Level level, const std::string& message) { std::string formatted = std::format("{} {} {}", time::get("(%d-%B-%Y %I:%M:%S)"), LEVEL_STRINGS[level], message); - std::println("{}", formatted); - if (file.is_open()) file << formatted << '\n' << std::flush; + write_raw(formatted); } - void Logger::info(const std::string& message) - { - write(INFO, message); - } + void Logger::info(const std::string& message) { write(INFO, message); } + void Logger::warning(const std::string& message) { write(WARNING, message); } + void Logger::error(const std::string& message) { write(ERROR, message); } + void Logger::fatal(const std::string& message) { write(FATAL, message); } + void Logger::command(const std::string& message) { write(COMMAND, message); } + void Logger::open(const std::filesystem::path& path) { file.open(path, std::ios::out | std::ios::app); } - void Logger::warning(const std::string& message) - { - write(WARNING, message); - } - - void Logger::error(const std::string& message) - { - write(ERROR, message); - } - - void Logger::fatal(const std::string& message) - { - write(FATAL, message); - } - - void Logger::command(const std::string& message) - { - write(COMMAND, message); - } - - void Logger::open(const std::filesystem::path& path) - { - file.open(path, std::ios::out | std::ios::app); - } + std::filesystem::path Logger::path() { return sdl::preferences_directory_get() / "log.txt"; } Logger::Logger() { - open(filesystem::path_preferences_get() / "log.txt"); + open(path()); info("Initializing Anm2Ed"); } @@ -57,7 +41,6 @@ namespace anm2ed info("Exiting Anm2Ed"); if (file.is_open()) file.close(); } - } anm2ed::Logger logger; diff --git a/src/log.h b/src/log.h index e9f84fa..f041520 100644 --- a/src/log.h +++ b/src/log.h @@ -46,6 +46,8 @@ namespace anm2ed std::ofstream file{}; public: + static std::filesystem::path path(); + void write_raw(const std::string&); void write(const Level, const std::string&); void info(const std::string&); void warning(const std::string&); diff --git a/src/manager.cpp b/src/manager.cpp index e807b20..4446ad4 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -5,8 +5,9 @@ #include -#include "filesystem_.h" #include "log.h" +#include "path_.h" +#include "sdl.h" #include "strings.h" #include "toast.h" #include "vector_.h" @@ -26,9 +27,7 @@ namespace anm2ed if (parent.empty()) return; std::error_code ec{}; std::filesystem::create_directories(parent, ec); - if (ec) - logger.warning( - std::format("Could not create directory for {}: {}", filesystem::path_to_utf8(path), ec.message())); + if (ec) logger.warning(std::format("Could not create directory for {}: {}", path::to_utf8(path), ec.message())); } } @@ -59,8 +58,8 @@ namespace anm2ed if (documents.empty()) selectionHistory.clear(); } - 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::recent_files_path_get() { return sdl::preferences_directory_get() / "recent.txt"; } + std::filesystem::path Manager::autosave_path_get() { return sdl::preferences_directory_get() / "autosave.txt"; } Manager::Manager() { @@ -74,7 +73,7 @@ namespace anm2ed { std::string errorString{}; documents.emplace_back(path, isNew, &errorString); - auto pathString = filesystem::path_to_utf8(path); + auto pathString = path::to_utf8(path); auto& document = documents.back(); if (!document.is_valid()) @@ -136,8 +135,7 @@ namespace anm2ed std::error_code ec{}; std::filesystem::remove(autosavePath, ec); if (ec) - logger.warning(std::format("Could not remove autosave file {}: {}", filesystem::path_to_utf8(autosavePath), - ec.message())); + logger.warning(std::format("Could not remove autosave file {}: {}", path::to_utf8(autosavePath), ec.message())); } autosave_files_write(); @@ -252,7 +250,7 @@ namespace anm2ed std::vector ordered; ordered.reserve(orderedEntries.size()); for (const auto& [pathString, _] : orderedEntries) - ordered.emplace_back(filesystem::path_from_utf8(pathString)); + ordered.emplace_back(path::from_utf8(pathString)); return ordered; } @@ -262,11 +260,11 @@ namespace anm2ed std::error_code ec{}; if (!std::filesystem::exists(path, ec)) { - logger.warning(std::format("Skipping missing recent file: {}", filesystem::path_to_utf8(path))); + logger.warning(std::format("Skipping missing recent file: {}", path::to_utf8(path))); return; } - recentFiles[filesystem::path_to_utf8(path)] = ++recentFilesCounter; + recentFiles[path::to_utf8(path)] = ++recentFilesCounter; recent_files_trim(); recent_files_write(); } @@ -278,12 +276,11 @@ namespace anm2ed std::ifstream file(path); if (!file) { - logger.warning( - std::format("Could not load recent files from: {}. Skipping...", filesystem::path_to_utf8(path))); + logger.warning(std::format("Could not load recent files from: {}. Skipping...", path::to_utf8(path))); return; } - logger.info(std::format("Loading recent files from: {}", filesystem::path_to_utf8(path))); + logger.info(std::format("Loading recent files from: {}", path::to_utf8(path))); std::string line{}; std::vector loaded{}; @@ -293,14 +290,14 @@ namespace anm2ed { if (line.empty()) continue; if (!line.empty() && line.back() == '\r') line.pop_back(); - auto entry = filesystem::path_from_utf8(line); + auto entry = path::from_utf8(line); std::error_code ec{}; if (!std::filesystem::exists(entry, ec)) { logger.warning(std::format("Skipping missing recent file: {}", line)); continue; } - auto entryString = filesystem::path_to_utf8(entry); + auto entryString = path::to_utf8(entry); if (!seen.insert(entryString).second) continue; loaded.emplace_back(std::move(entryString)); } @@ -324,14 +321,13 @@ namespace anm2ed if (!file.is_open()) { - logger.warning( - std::format("Could not write recent files to: {}. Skipping...", filesystem::path_to_utf8(path))); + logger.warning(std::format("Could not write recent files to: {}. Skipping...", path::to_utf8(path))); return; } auto ordered = recent_files_ordered(); for (auto& entry : ordered) - file << filesystem::path_to_utf8(entry) << '\n'; + file << path::to_utf8(entry) << '\n'; } void Manager::recent_files_clear() @@ -346,9 +342,9 @@ namespace anm2ed for (auto& path : autosaveFiles) { auto fileNamePath = path.filename(); - auto fileNameUtf8 = filesystem::path_to_utf8(fileNamePath); + auto fileNameUtf8 = path::to_utf8(fileNamePath); if (!fileNameUtf8.empty() && fileNameUtf8.front() == '.') fileNameUtf8.erase(fileNameUtf8.begin()); - fileNamePath = filesystem::path_from_utf8(fileNameUtf8); + fileNamePath = path::from_utf8(fileNameUtf8); auto restorePath = path.parent_path() / fileNamePath; restorePath.replace_extension(""); @@ -372,12 +368,11 @@ namespace anm2ed std::ifstream file(path); if (!file) { - logger.warning( - std::format("Could not load autosave files from: {}. Skipping...", filesystem::path_to_utf8(path))); + logger.warning(std::format("Could not load autosave files from: {}. Skipping...", path::to_utf8(path))); return; } - logger.info(std::format("Loading autosave files from: {}", filesystem::path_to_utf8(path))); + logger.info(std::format("Loading autosave files from: {}", path::to_utf8(path))); std::string line{}; @@ -385,7 +380,7 @@ namespace anm2ed { if (line.empty()) continue; if (!line.empty() && line.back() == '\r') line.pop_back(); - auto entry = filesystem::path_from_utf8(line); + auto entry = path::from_utf8(line); if (std::find(autosaveFiles.begin(), autosaveFiles.end(), entry) != autosaveFiles.end()) continue; autosaveFiles.emplace_back(std::move(entry)); } @@ -398,7 +393,7 @@ namespace anm2ed autosaveWriteFile.open(autosave_path_get(), std::ofstream::out | std::ofstream::trunc); for (auto& path : autosaveFiles) - autosaveWriteFile << filesystem::path_to_utf8(path) << "\n"; + autosaveWriteFile << path::to_utf8(path) << "\n"; autosaveWriteFile.close(); } diff --git a/src/manager.h b/src/manager.h index f272f5a..79183a8 100644 --- a/src/manager.h +++ b/src/manager.h @@ -35,6 +35,7 @@ namespace anm2ed int recordingStart{}; int recordingEnd{}; bool isRecordingRange{}; + bool isAbleToRecord{}; ImGuiKeyChord chords[SHORTCUT_COUNT]{}; diff --git a/src/render.cpp b/src/render.cpp index ba5618f..9f5e6b2 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -5,26 +5,17 @@ #include #include -#ifdef _WIN32 - #include "string_.h" - #define POPEN _popen - #define PCLOSE _pclose -constexpr auto PWRITE_MODE = "wb"; -#elif __unix__ - #define POPEN popen - #define PCLOSE pclose -constexpr auto PWRITE_MODE = "w"; -#endif - #include "log.h" +#include "process_.h" +#include "sdl.h" +#include "string_.h" using namespace anm2ed::resource; +using namespace anm2ed::util; using namespace glm; namespace anm2ed { - constexpr auto FFMPEG_POPEN_ERROR = "popen() (for FFmpeg) failed!\n{}"; - bool animation_render(const std::string& ffmpegPath, const std::string& path, std::vector& frames, AudioStream& audioStream, render::Type type, ivec2 size, int fps) { @@ -35,7 +26,7 @@ namespace anm2ed std::string audioOutputArguments{"-an"}; std::string command{}; - auto remove_audio_file = [&]() + auto audio_remove = [&]() { if (!audioPath.empty()) { @@ -76,7 +67,7 @@ namespace anm2ed else { logger.warning("Failed to open temporary audio file; exporting video without audio."); - remove_audio_file(); + audio_remove(); } } @@ -108,18 +99,22 @@ namespace anm2ed return false; } + command += " 2>&1"; + #if _WIN32 - command = util::string::quote(command); + command = "powershell -Command \"& " + command + "\""; + command += " | Tee-Object -FilePath " + string::quote(Logger::path().string()) + " -Append"; +#else + command += " | tee -a " + string::quote(Logger::path().string()); #endif logger.command(command); - FILE* fp = POPEN(command.c_str(), PWRITE_MODE); + Process process(command.c_str(), "w"); - if (!fp) + if (!process.get()) { - remove_audio_file(); - logger.error(std::format(FFMPEG_POPEN_ERROR, strerror(errno))); + audio_remove(); return false; } @@ -127,16 +122,16 @@ namespace anm2ed { auto frameSize = frame.pixel_size_get(); - if (fwrite(frame.pixels.data(), 1, frameSize, fp) != frameSize) + if (fwrite(frame.pixels.data(), 1, frameSize, process.get()) != frameSize) { - remove_audio_file(); - PCLOSE(fp); + audio_remove(); return false; } } - auto code = PCLOSE(fp); - remove_audio_file(); - return (code == 0); + process.close(); + + audio_remove(); + return true; } } diff --git a/src/render.h b/src/render.h index b5b7985..b72d861 100644 --- a/src/render.h +++ b/src/render.h @@ -35,6 +35,7 @@ namespace anm2ed::render namespace anm2ed { + std::filesystem::path ffmpeg_log_path(); 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.cpp b/src/resource/audio.cpp index 6d945c3..41e45bc 100644 --- a/src/resource/audio.cpp +++ b/src/resource/audio.cpp @@ -1,14 +1,11 @@ #include "audio.h" -#include -#include -#include #include #include -#include "filesystem_.h" +#include "file_.h" -namespace filesystem = anm2ed::util::filesystem; +using namespace anm2ed::util; namespace anm2ed::resource { @@ -22,7 +19,7 @@ namespace anm2ed::resource { if (path.empty()) return; - filesystem::File file(path, "rb"); + File file(path, "rb"); if (!file) return; if (std::fseek(file.get(), 0, SEEK_END) != 0) return; diff --git a/src/resource/strings.h b/src/resource/strings.h index 95da616..d1c5bbc 100644 --- a/src/resource/strings.h +++ b/src/resource/strings.h @@ -406,14 +406,12 @@ namespace anm2ed X(TOAST_EXPORT_SPRITESHEET_FAILED, "Could not export spritesheet to: {0}", "No se pudo exportar spritesheet a: {0}", "Не удалось экспортировать спрайт-лист в: {0}", "无法导出图集至: {0}", "{0}에 스프라이트 시트를 내보내기 할 수 없습니다.") \ X(TOAST_ADD_SPRITESHEET_FAILED, "Failed to add spritesheet! Open a document first.", "¡Error a añadir spritesheet! Abre un documento primero.", "Не удалось добавить спрайт-лист! Сначала откройте документ.", "无法添加图集!请先打开文档。", "스프라이트 시트를 추가할 수 없습니다! 먼저 문서를 여세요.") \ X(TOAST_ADD_SOUND_FAILED, "Failed to add sound! Open a document first.", "¡Error al añadir sonido! Abre un documento primero.", "Не удалось добавить звук! Сначала откройте документ.", "无法添加声音!请先打开文档。", "사운드를 추가할 수 없습니다! 먼저 문서를 여세요.") \ - X(TOAST_INVALID_FFMPEG_PATH, "FFmpeg path must point to a valid executable file.", "La ruta de FFmpeg debe apuntar a un archivo ejecutable valido.", "Путь к FFmpeg должен указывать к валидному файлу .exe.", "FFmpeg路径必须指向一个正确的可执行文件.", "FFmpeg 경로는 유효한 실행 파일을 지정해야 합니다.") \ + X(TOAST_INVALID_FFMPEG, "Unable to run FFmpeg. Make sure the executable exists, has the proper permissions, and is a valid FFmpeg executable.", "No se pudo ejecutar FFmpeg. Asegúrate de que el ejecutable exista, tenga los permisos adecuados y sea un FFmpeg válido.", "Не удалось запустить FFmpeg. Убедитесь, что исполняемый файл существует, имеет нужные права и является корректным исполняемым FFmpeg.", "无法运行 FFmpeg。请确保可执行文件存在,具有正确的权限,并且是有效的 FFmpeg 可执行文件。", "FFmpeg을 실행할 수 없습니다. 실행 파일이 존재하고 올바른 권한을 가지며 유효한 FFmpeg 실행 파일인지 확인하세요.") \ X(TOAST_NOT_SUPPORTED, "Operation not supported.", "Operacion no soportada.", "Операция не поддерживается.", "不支持此操作.", "해당 작업은 지원되지 않습니다.") \ X(TOAST_OPEN_DOCUMENT, "Opened document: {0}", "Documento Abierto: {0}", "Открыт документ: {0}", "已打开文件: {0}", "{0} 파일 열기") \ X(TOAST_OPEN_DOCUMENT_FAILED, "Failed to open document: {0} ({1})", "Error al abrir el documento: {0} ({1})", "Не удалось открыть документ: {0} ({1})", "打开文件失败: {0} ({1})", "{0} 파일을 여는데 실패했습니다. {1}") \ - X(TOAST_PNG_DIRECTORY_ACCESS_ERROR, "Could not access directory: {0} ({1})", "No se pudo acceder al directorio: {0} ({1})", "Не удалось получить доступ к директории: {0} ({1})", "无法访问目录: {0} ({1})", "{0} 디렉터리에 접근할 수 없습니다. {1}") \ - X(TOAST_PNG_DIRECTORY_CREATE_ERROR, "Could not create directory: {0} ({1})", "No se pudo crear el directorio: {0} ({1})", "Не удалось создать директорию: {0} ({1})", "无法创造目录: {0} ({1})", "{0} 디렉터리를 생성할 수 없습니다. {1}") \ - X(TOAST_PNG_DIRECTORY_NOT_DIRECTORY, "PNG output path must be a directory: {0}", "La ruta de salida de PNG debe ser un directorio: {0}", "Выходной путь для PNG должен быть директорией: {0}", "PNG输出路径必须是一个目录: {0}", "PNG를 출력할 경로는 디렉터리여야 합니다. {0}") \ - X(TOAST_PNG_DIRECTORY_NOT_SET, "PNG output directory must be set.", "El directorio de salida de PNG debe ser ajustado.", "Директория для вывода PNG должна быть установлена.", "必须设置PNG输出目录.", "PNG를 출력할 경로를 설정해야 합니다.") \ + X(TOAST_PNG_DIRECTORY_INVALID, "Unable to create PNG directory. Make sure to check permissions.", "No se puede crear el directorio PNG. Verifica los permisos.", "Не удалось создать каталог PNG. Проверьте права доступа.", "无法创建 PNG 目录。请检查权限。", "PNG 디렉터리를 만들 수 없습니다. 권한을 확인하세요.") \ + X(TOAST_PNG_FORMAT_INVALID, "PNG format invalid. Make sure it contains \"{}\".", "Formato PNG inválido. Asegúrate de que contenga \"{}\".", "Недопустимый формат PNG. Убедитесь, что он содержит \"{}\".", "PNG 格式无效。请确保其中包含 \"{}\"。", "PNG 형식이 잘못되었습니다. \"{}\"을 포함하는지 확인하세요.") \ X(TOAST_REDO, "Redo: {0}", "Rehacer: {0}", "Повтор: {0}", "重做: {0}", "다시 실행: {0}") \ X(ERROR_FILE_NOT_FOUND, "File not found!", "¡Archivo no encontrado!", "Файл не найден!", "找不到文件!", "파일을 찾을 수 없습니다!") \ X(TOAST_RELOAD_SPRITESHEET, "Reloaded spritesheet #{0}: {1}", "Se ha recargado spritesheet #{0}: {1}", "Спрайт-лист #{0} перезагружен: {1}", "重新加载了图集 #{0}: {1}", "{0}번 스프라이트 시트 다시 불러옴: {1}") \ @@ -429,6 +427,7 @@ namespace anm2ed X(TOAST_SOUNDS_PASTE, "Paste Sound(s)", "Pegar Sonido(s)", "Вставить звуки", "粘贴声音", "사운드 붙여넣기") \ X(TOAST_SOUND_INITIALIZED, "Initialized sound #{0}: {1}", "Sonido Inizializado #{0}: {1}", "Звук #{0} инициализирован: {1}", "已初始化声音 #{0}: {1}", "{0}번 사운드 초기화됨: {1}") \ X(TOAST_SOUND_INITIALIZE_FAILED, "Failed to initialize sound: {0}", "Error al Inizializar sonido: {0}", "Не удалось инициализировать звуки: {0}", "初始化声音失败: {0}", "사운드 초기화 실패: {0}") \ + X(TOAST_RENDER_PATH_EMPTY, "Render path is empty!", "¡La ruta de render está vacía!", "Путь вывода пуст!", "渲染路径为空!", "렌더 경로가 비어 있습니다!") \ X(TOAST_SPRITESHEET_EMPTY, "Spritesheet export failed: captured frames are empty.", "Error al exportar spritesheet: Frames capturados estan vacios.", "Не удалось экспортировать спрайт-лист: захваченные кадры пусты.", "导出图集失败: 未捕获任何帧.", "스프라이트 내보내기 실패: 캡처된 프레임이 비어있습니다.") \ X(TOAST_SPRITESHEET_INITIALIZED, "Initialized spritesheet #{0}: {1}", "Spritesheet Inizializada #{0}: {1}", "Спрайт-лист #{0} инициализирован: {1}", "已初始化图集 #{0}: {1}", "{0}번 스프라이트 시트 초기화됨: {1}") \ X(TOAST_SPRITESHEET_INIT_FAILED, "Failed to initialize spritesheet: {0}", "Error al Inizializar spritesheet: {0}", "Не удалось инициализировать спрайт-лист: {0}", "初始化图集失败: {0}", "스프라이트 시트 초기화 실패: {0}") \ diff --git a/src/resource/texture.cpp b/src/resource/texture.cpp index 9a99144..ac4a439 100644 --- a/src/resource/texture.cpp +++ b/src/resource/texture.cpp @@ -22,13 +22,13 @@ #pragma GCC diagnostic pop #endif -#include "filesystem_.h" +#include "file_.h" #include "math_.h" using namespace anm2ed::resource::texture; using namespace anm2ed::util::math; +using namespace anm2ed::util; using namespace glm; -namespace filesystem = anm2ed::util::filesystem; namespace anm2ed::resource { @@ -119,10 +119,10 @@ namespace anm2ed::resource Texture::Texture(const std::filesystem::path& pngPath) { - filesystem::File file(pngPath, "rb"); + File file(pngPath, "rb"); if (auto handle = file.get()) { - if (const uint8_t* data = stbi_load_from_file(handle, &size.x, &size.y, nullptr, CHANNELS); data) + if (auto data = stbi_load_from_file(handle, &size.x, &size.y, nullptr, CHANNELS); data) { upload(data); stbi_image_free((void*)data); @@ -134,7 +134,7 @@ namespace anm2ed::resource { if (pixels.empty()) return false; - filesystem::File file(path, "wb"); + File file(path, "wb"); if (auto handle = file.get()) { auto write_func = [](void* context, void* data, int size) { fwrite(data, 1, size, static_cast(context)); }; diff --git a/src/settings.cpp b/src/settings.cpp index e3529f2..e59d9b1 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -3,12 +3,11 @@ #include #include -#include "filesystem_.h" #include "log.h" +#include "path_.h" using namespace anm2ed::util; using namespace glm; -namespace filesystem = anm2ed::util::filesystem; namespace anm2ed { @@ -123,8 +122,8 @@ DockSpace ID=0x123F8F08 Window=0x6D581B32 Pos=8,62 Size=1902,994 Spl Settings::Settings(const std::filesystem::path& path) { - auto pathUtf8 = filesystem::path_to_utf8(path); - if (filesystem::path_is_exist(path)) + auto pathUtf8 = path::to_utf8(path); + if (path::is_exist(path)) logger.info(std::format("Using settings from: {}", pathUtf8)); else { @@ -271,7 +270,7 @@ DockSpace ID=0x123F8F08 Window=0x6D581B32 Pos=8,62 Size=1902,994 Spl std::string Settings::imgui_data_load(const std::filesystem::path& path) { - auto pathUtf8 = filesystem::path_to_utf8(path); + auto pathUtf8 = path::to_utf8(path); std::ifstream file(path, std::ios::in | std::ios::binary); if (!file.is_open()) { diff --git a/src/settings.h b/src/settings.h index 253d727..5d09ef4 100644 --- a/src/settings.h +++ b/src/settings.h @@ -29,6 +29,7 @@ namespace anm2ed X(BOOL, bool) \ X(FLOAT, float) \ X(STRING, std::string) \ + X(PATH, std::filesystem::path) \ X(IVEC2, glm::ivec2) \ X(IVEC2_WH, glm::ivec2) \ X(VEC2, glm::vec2) \ @@ -69,25 +70,26 @@ namespace anm2ed X(PLAYBACK_IS_LOOP, playbackIsLoop, STRING_UNDEFINED, BOOL, true) \ X(PLAYBACK_IS_CLAMP, playbackIsClamp, STRING_UNDEFINED, BOOL, true) \ \ - X(CHANGE_IS_CROP, changeIsCrop, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_CROP_X, changeIsCropX, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_CROP_Y, changeIsCropY, STRING_UNDEFINED, BOOL, false) \ - X(CHANGE_IS_SIZE, changeIsSize, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_SIZE_X, changeIsSizeX, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_SIZE_Y, changeIsSizeY, STRING_UNDEFINED, BOOL, false) \ - X(CHANGE_IS_POSITION, changeIsPosition, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_POSITION_X, changeIsPositionX, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_POSITION_Y, changeIsPositionY, STRING_UNDEFINED, BOOL, false) \ - X(CHANGE_IS_PIVOT, changeIsPivot, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_PIVOT_X, changeIsPivotX, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_PIVOT_Y, changeIsPivotY, STRING_UNDEFINED, BOOL, false) \ - X(CHANGE_IS_SCALE, changeIsScale, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_SCALE_X, changeIsScaleX, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_SCALE_Y, changeIsScaleY, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_ROTATION, changeIsRotation, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_DURATION, changeIsDuration, STRING_UNDEFINED, BOOL, false) \ - X(CHANGE_IS_TINT, changeIsTint, STRING_UNDEFINED, BOOL, false) \ - X(CHANGE_IS_COLOR_OFFSET, changeIsColorOffset, STRING_UNDEFINED, BOOL, false) \ + X(CHANGE_IS_TINT_R, changeIsTintR, STRING_UNDEFINED, BOOL, false) \ + X(CHANGE_IS_TINT_G, changeIsTintG, STRING_UNDEFINED, BOOL, false) \ + X(CHANGE_IS_TINT_B, changeIsTintB, STRING_UNDEFINED, BOOL, false) \ + X(CHANGE_IS_TINT_A, changeIsTintA, STRING_UNDEFINED, BOOL, false) \ + X(CHANGE_IS_COLOR_OFFSET_R, changeIsColorOffsetR, STRING_UNDEFINED, BOOL, false) \ + X(CHANGE_IS_COLOR_OFFSET_G, changeIsColorOffsetG, STRING_UNDEFINED, BOOL, false) \ + X(CHANGE_IS_COLOR_OFFSET_B, changeIsColorOffsetB, STRING_UNDEFINED, BOOL, false) \ + X(CHANGE_IS_COLOR_OFFSET_A, changeIsColorOffsetA, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_VISIBLE_SET, changeIsVisibleSet, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_INTERPOLATED_SET, changeIsInterpolatedSet, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_CROP, changeCrop, STRING_UNDEFINED, VEC2, {}) \ @@ -101,8 +103,6 @@ namespace anm2ed X(CHANGE_COLOR_OFFSET, changeColorOffset, STRING_UNDEFINED, VEC3, {}) \ X(CHANGE_IS_VISIBLE, changeIsVisible, STRING_UNDEFINED, BOOL, false) \ X(CHANGE_IS_INTERPOLATED, changeIsInterpolated, STRING_UNDEFINED, BOOL, false) \ - X(CHANGE_NUMBER_FRAMES, changeNumberFrames, STRING_UNDEFINED, INT, 1) \ - X(CHANGE_IS_FROM_SELECTED_FRAME, changeIsFromSelectedFrame, STRING_UNDEFINED, BOOL, false) \ \ X(SCALE_VALUE, scaleValue, STRING_UNDEFINED, FLOAT, 1.0f) \ \ @@ -167,13 +167,13 @@ namespace anm2ed X(TOOL_COLOR, toolColor, STRING_UNDEFINED, VEC4, {1.0, 1.0, 1.0, 1.0}) \ \ X(RENDER_TYPE, renderType, STRING_UNDEFINED, INT, render::GIF) \ - X(RENDER_PATH, renderPath, STRING_UNDEFINED, STRING, OUTPUT_PATH_DEFAULT) \ + X(RENDER_PATH, renderPath, STRING_UNDEFINED, PATH, OUTPUT_PATH_DEFAULT) \ X(RENDER_ROWS, renderRows, STRING_UNDEFINED, INT, 0) \ X(RENDER_COLUMNS, renderColumns, STRING_UNDEFINED, INT, 0) \ - X(RENDER_FORMAT, renderFormat, STRING_UNDEFINED, STRING, "{}.png") \ + X(RENDER_FORMAT, renderFormat, STRING_UNDEFINED, PATH, "{}.png") \ X(RENDER_IS_RAW_ANIMATION, renderIsRawAnimation, STRING_UNDEFINED, BOOL, true) \ X(RENDER_SCALE, renderScale, STRING_UNDEFINED, FLOAT, 1.0f) \ - X(RENDER_FFMPEG_PATH, renderFFmpegPath, STRING_UNDEFINED, STRING, FFMPEG_PATH_DEFAULT) + X(RENDER_FFMPEG_PATH, renderFFmpegPath, STRING_UNDEFINED, PATH, FFMPEG_PATH_DEFAULT) #define SETTINGS_SHORTCUTS \ /* Symbol / Name / String / Type / Default */ \ diff --git a/src/state.cpp b/src/state.cpp index 037bc67..9072c2c 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -5,8 +5,8 @@ #include #include -#include "filesystem_.h" #include "log.h" +#include "path_.h" #include "strings.h" #include "toast.h" @@ -26,7 +26,7 @@ namespace anm2ed dialog = Dialog(window); for (const auto& argument : arguments) - manager.open(filesystem::path_from_utf8(argument)); + manager.open(path::from_utf8(argument)); manager.chords_set(settings); } @@ -46,8 +46,8 @@ namespace anm2ed { std::string droppedFile = event.drop.data ? event.drop.data : ""; if (droppedFile.empty()) break; - auto droppedPath = filesystem::path_from_utf8(droppedFile); - if (filesystem::path_is_extension(droppedPath, "anm2")) + auto droppedPath = path::from_utf8(droppedFile); + if (path::is_extension(droppedPath, "anm2")) { if (manager.documents.empty()) manager.open(droppedPath); @@ -60,7 +60,7 @@ namespace anm2ed } SDL_FlashWindow(window, SDL_FLASH_UNTIL_FOCUSED); } - else if (filesystem::path_is_extension(droppedPath, "png")) + else if (path::is_extension(droppedPath, "png")) { if (auto document = manager.get()) document->spritesheet_add(droppedPath); @@ -70,8 +70,7 @@ namespace anm2ed logger.warning(localize.get(TOAST_ADD_SPRITESHEET_FAILED, anm2ed::ENGLISH)); } } - else if (filesystem::path_is_extension(droppedPath, "wav") || - filesystem::path_is_extension(droppedPath, "ogg")) + else if (path::is_extension(droppedPath, "wav") || path::is_extension(droppedPath, "ogg")) { if (auto document = manager.get()) document->sound_add(droppedPath); @@ -87,8 +86,8 @@ namespace anm2ed { std::string droppedFile = event.drop.data ? event.drop.data : ""; if (droppedFile.empty()) break; - auto droppedPath = filesystem::path_from_utf8(droppedFile); - if (filesystem::path_is_extension(droppedPath, "anm2")) + auto droppedPath = path::from_utf8(droppedFile); + if (path::is_extension(droppedPath, "anm2")) { manager.open(droppedPath); SDL_FlashWindow(window, SDL_FLASH_UNTIL_FOCUSED); diff --git a/src/util/file_.cpp b/src/util/file_.cpp new file mode 100644 index 0000000..f67887b --- /dev/null +++ b/src/util/file_.cpp @@ -0,0 +1,41 @@ +#include "file_.h" + +namespace anm2ed::util +{ + File::File(const std::filesystem::path& path, const char* mode) { open(path, mode); } + + File::~File() { close(); } + + bool File::open(const std::filesystem::path& path, const char* mode) + { + close(); + +#ifdef _WIN32 + std::wstring wideMode{}; + if (mode) + wideMode.assign(mode, mode + std::strlen(mode)); + else + wideMode = L"rb"; + handle = _wfopen(path.native().c_str(), wideMode.c_str()); +#else + handle = std::fopen(path.string().c_str(), mode); +#endif + + return handle != nullptr; + } + + int File::close() + { + if (handle) + { + auto result = std::fclose(handle); + handle = nullptr; + return result; + } + return -1; + } + + FILE* File::get() const { return handle; } + + File::operator bool() const { return handle != nullptr; } +} \ No newline at end of file diff --git a/src/util/file_.h b/src/util/file_.h new file mode 100644 index 0000000..3e3ce07 --- /dev/null +++ b/src/util/file_.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +namespace anm2ed::util +{ + class File + { + public: + File() = default; + File(const std::filesystem::path&, const char* mode); + ~File(); + + bool open(const std::filesystem::path&, const char* mode); + int close(); + FILE* get() const; + explicit operator bool() const; + + private: + FILE* handle{}; + }; +} \ No newline at end of file diff --git a/src/util/filesystem_.cpp b/src/util/filesystem_.cpp deleted file mode 100644 index 975ef00..0000000 --- a/src/util/filesystem_.cpp +++ /dev/null @@ -1,140 +0,0 @@ -#include "filesystem_.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace anm2ed::util::filesystem -{ - namespace - { - template CharT to_lower_char(CharT character) - { - if constexpr (std::is_same_v) - return static_cast(std::towlower(static_cast(character))); - else - return static_cast(std::tolower(static_cast(character))); - } - } - - std::filesystem::path path_preferences_get() - { - auto sdlPath = SDL_GetPrefPath(nullptr, "anm2ed"); - if (!sdlPath) return {}; - auto filePath = path_from_utf8(sdlPath); - SDL_free(sdlPath); - return filePath; - } - - std::filesystem::path path_to_lower(const std::filesystem::path& path) - { - auto native = path.native(); - for (auto& character : native) - character = to_lower_char(character); - return std::filesystem::path(native); - } - - std::filesystem::path path_backslash_replace(const std::filesystem::path& path) - { - auto native = path.native(); - constexpr auto backslash = static_cast('\\'); - constexpr auto slash = static_cast('/'); - for (auto& character : native) - if (character == backslash) character = slash; - return std::filesystem::path(native); - } - - std::string path_to_utf8(const std::filesystem::path& path) - { - auto u8 = path.u8string(); - return std::string(u8.begin(), u8.end()); - } - - std::filesystem::path path_from_utf8(const std::string& utf8) - { - return std::filesystem::path(std::u8string(utf8.begin(), utf8.end())); - } - - FILE* open(const std::filesystem::path& path, const char* mode) - { -#ifdef _WIN32 - std::wstring wideMode{}; - if (mode) - wideMode.assign(mode, mode + std::strlen(mode)); - else - wideMode = L"rb"; - return _wfopen(path.native().c_str(), wideMode.c_str()); -#else - return std::fopen(path.string().c_str(), mode); -#endif - } - - bool path_is_exist(const std::filesystem::path& path) - { - std::error_code errorCode; - return std::filesystem::exists(path, errorCode) && ((void)std::filesystem::status(path, errorCode), !errorCode); - } - - bool path_is_extension(const std::filesystem::path& path, const std::string& extension) - { - auto extensionUtf8 = path_to_utf8(std::filesystem::path(path).extension()); - std::transform(extensionUtf8.begin(), extensionUtf8.end(), extensionUtf8.begin(), ::tolower); - return extensionUtf8 == ("." + extension); - } - - std::filesystem::path path_lower_case_backslash_handle(const std::filesystem::path& path) - { - auto newPath = path; - if (path_is_exist(newPath)) return newPath; - - newPath = path_backslash_replace(newPath); - if (path_is_exist(newPath)) return newPath; - - newPath = path_to_lower(newPath); - return newPath; - } - - WorkingDirectory::WorkingDirectory(const std::filesystem::path& path, bool isFile) - { - previous = std::filesystem::current_path(); - if (isFile) - { - std::filesystem::path filePath = path; - std::filesystem::path parentPath = filePath.parent_path(); - std::filesystem::current_path(parentPath); - } - else - std::filesystem::current_path(path); - } - - WorkingDirectory::~WorkingDirectory() { std::filesystem::current_path(previous); } - - File::File(const std::filesystem::path& path, const char* mode) { open(path, mode); } - - File::~File() { close(); } - - bool File::open(const std::filesystem::path& path, const char* mode) - { - close(); - handle = filesystem::open(path, mode); - return handle != nullptr; - } - - void File::close() - { - if (handle) - { - std::fclose(handle); - handle = nullptr; - } - } - - FILE* File::get() const { return handle; } - - File::operator bool() const { return handle != nullptr; } -} diff --git a/src/util/filesystem_.h b/src/util/filesystem_.h deleted file mode 100644 index 9f72fea..0000000 --- a/src/util/filesystem_.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace anm2ed::util::filesystem -{ - std::filesystem::path path_preferences_get(); - std::filesystem::path path_to_lower(const std::filesystem::path&); - std::filesystem::path path_backslash_replace(const std::filesystem::path&); - std::string path_to_utf8(const std::filesystem::path&); - std::filesystem::path path_from_utf8(const std::string&); - - bool path_is_exist(const std::filesystem::path&); - bool path_is_extension(const std::filesystem::path&, const std::string&); - - std::filesystem::path path_lower_case_backslash_handle(const std::filesystem::path&); - - FILE* open(const std::filesystem::path&, const char*); - - class File - { - public: - File() = default; - File(const std::filesystem::path&, const char* mode); - ~File(); - - bool open(const std::filesystem::path&, const char* mode); - void close(); - FILE* get() const; - explicit operator bool() const; - - private: - FILE* handle{}; - }; - - class WorkingDirectory - { - public: - std::filesystem::path previous; - - WorkingDirectory(const std::filesystem::path&, bool = false); - ~WorkingDirectory(); - }; - -} diff --git a/src/util/path_.cpp b/src/util/path_.cpp new file mode 100644 index 0000000..a98188a --- /dev/null +++ b/src/util/path_.cpp @@ -0,0 +1,120 @@ +#include "path_.h" + +#include +#include +#include +#include +#include +#include + +namespace anm2ed::util::path +{ + namespace + { + template CharT to_lower_char(CharT character) + { + if constexpr (std::is_same_v) + return static_cast(std::towlower(static_cast(character))); + else + return static_cast(std::tolower(static_cast(character))); + } + } + + std::filesystem::path to_lower(const std::filesystem::path& path) + { + auto native = path.native(); + for (auto& character : native) + character = to_lower_char(character); + return std::filesystem::path(native); + } + + std::filesystem::path backslash_replace(const std::filesystem::path& path) + { + auto native = path.native(); + constexpr auto backslash = static_cast('\\'); + constexpr auto slash = static_cast('/'); + for (auto& character : native) + if (character == backslash) character = slash; + return std::filesystem::path(native); + } + + std::string to_utf8(const std::filesystem::path& path) + { + auto u8 = path.u8string(); + return std::string(u8.begin(), u8.end()); + } + + std::filesystem::path from_utf8(const std::string& utf8) + { + return std::filesystem::path(std::u8string(utf8.begin(), utf8.end())); + } + + bool is_exist(const std::filesystem::path& path) + { + if (path.empty()) return false; + std::error_code errorCode; + return std::filesystem::exists(path, errorCode) && ((void)std::filesystem::status(path, errorCode), !errorCode); + } + + bool is_extension(const std::filesystem::path& path, const std::string& extension) + { + if (path.empty()) return false; + auto extensionUtf8 = to_utf8(std::filesystem::path(path).extension()); + std::transform(extensionUtf8.begin(), extensionUtf8.end(), extensionUtf8.begin(), ::tolower); + return extensionUtf8 == ("." + extension); + } + + bool is_executable(const std::filesystem::path& path) + { + if (path.empty()) return false; + + std::error_code ec; + auto status = std::filesystem::status(path, ec); + if (ec) return false; + if (!std::filesystem::exists(status)) return false; + if (!std::filesystem::is_regular_file(status)) return false; + +#ifdef WIN32 + auto extension = to_lower(path.extension()); + if (extension != ".exe" && extension != ".bat" && extension != ".cmd") return false; +#else + auto permissions = status.permissions(); + auto executableMask = + std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec; + if ((permissions & executableMask) == std::filesystem::perms::none) return false; +#endif + + return true; + } + + bool ensure_directory(const std::filesystem::path& path) + { + if (path.empty()) return false; + + std::error_code ec; + if (std::filesystem::create_directories(path, ec)) return true; + + return is_exist(path.parent_path()); + } + + std::filesystem::path make_relative(const std::filesystem::path& path) + { + if (path.empty()) return path; + std::error_code ec{}; + auto relative = std::filesystem::relative(path, ec); + if (!ec) return relative; + return path; + } + + std::filesystem::path lower_case_backslash_handle(const std::filesystem::path& path) + { + auto newPath = path; + if (is_exist(newPath)) return newPath; + + newPath = backslash_replace(newPath); + if (is_exist(newPath)) return newPath; + + newPath = to_lower(newPath); + return newPath; + } +} diff --git a/src/util/path_.h b/src/util/path_.h new file mode 100644 index 0000000..5665029 --- /dev/null +++ b/src/util/path_.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +namespace anm2ed::util::path +{ + std::filesystem::path to_lower(const std::filesystem::path&); + std::filesystem::path backslash_replace(const std::filesystem::path&); + std::string to_utf8(const std::filesystem::path&); + std::filesystem::path from_utf8(const std::string&); + std::filesystem::path make_relative(const std::filesystem::path&); + + bool is_exist(const std::filesystem::path&); + bool is_executable(const std::filesystem::path&); + bool is_extension(const std::filesystem::path&, const std::string&); + + bool ensure_directory(const std::filesystem::path&); + + std::filesystem::path lower_case_backslash_handle(const std::filesystem::path&); +} diff --git a/src/util/process_.cpp b/src/util/process_.cpp new file mode 100644 index 0000000..f3b9bf6 --- /dev/null +++ b/src/util/process_.cpp @@ -0,0 +1,55 @@ +#include "process_.h" + +namespace anm2ed::util +{ + Process::Process(const char* command, const char* mode) { open(command, mode); } + + Process::~Process() { close(); } + + bool Process::open(const char* command, const char* mode) + { + close(); + +#ifdef WIN32 + pipe = _popen(command, mode); +#else + pipe = popen(command, mode); +#endif + return pipe != nullptr; + } + + int Process::close() + { + if (pipe) + { +#ifdef WIN32 + auto result = _pclose(pipe); +#else + auto result = pclose(pipe); +#endif + pipe = nullptr; + + return result; + } + + return -1; + } + + std::string Process::output_get_and_close() + { + std::string output{}; + if (!pipe) return {}; + + char buffer[256]; + while (fgets(buffer, sizeof(buffer), pipe)) + output += buffer; + + close(); + + return output; + } + + FILE* Process::get() const { return pipe; } + + Process::operator bool() const { return pipe != nullptr; } +} \ No newline at end of file diff --git a/src/util/process_.h b/src/util/process_.h new file mode 100644 index 0000000..d3654ae --- /dev/null +++ b/src/util/process_.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace anm2ed::util +{ + class Process + { + public: + Process() = default; + Process(const char*, const char*); + ~Process(); + + bool open(const char*, const char*); + int close(); + FILE* get() const; + explicit operator bool() const; + + std::string output_get_and_close(); + + private: + FILE* pipe{}; + }; +} \ No newline at end of file diff --git a/src/util/sdl.cpp b/src/util/sdl.cpp new file mode 100644 index 0000000..a4ff66a --- /dev/null +++ b/src/util/sdl.cpp @@ -0,0 +1,17 @@ +#include "sdl.h" + +#include + +#include "path_.h" + +namespace anm2ed::util::sdl +{ + std::filesystem::path preferences_directory_get() + { + auto sdlPath = SDL_GetPrefPath(nullptr, "anm2ed"); + if (!sdlPath) return {}; + auto filePath = path::from_utf8(sdlPath); + SDL_free(sdlPath); + return filePath; + } +} \ No newline at end of file diff --git a/src/util/sdl.h b/src/util/sdl.h new file mode 100644 index 0000000..64d40a7 --- /dev/null +++ b/src/util/sdl.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace anm2ed::util::sdl +{ + std::filesystem::path preferences_directory_get(); +} \ No newline at end of file diff --git a/src/util/working_directory.cpp b/src/util/working_directory.cpp new file mode 100644 index 0000000..91ee5ac --- /dev/null +++ b/src/util/working_directory.cpp @@ -0,0 +1,19 @@ +#include "working_directory.h" + +namespace anm2ed::util +{ + WorkingDirectory::WorkingDirectory(const std::filesystem::path& path, Type type) + { + previous = std::filesystem::current_path(); + if (type == FILE) + { + std::filesystem::path filePath = path; + std::filesystem::path parentPath = filePath.parent_path(); + std::filesystem::current_path(parentPath); + } + else + std::filesystem::current_path(path); + } + + WorkingDirectory::~WorkingDirectory() { std::filesystem::current_path(previous); } +} \ No newline at end of file diff --git a/src/util/working_directory.h b/src/util/working_directory.h new file mode 100644 index 0000000..55974f0 --- /dev/null +++ b/src/util/working_directory.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace anm2ed::util +{ + class WorkingDirectory + { + public: + enum Type + { + FILE, + DIRECTORY + }; + + std::filesystem::path previous{}; + + WorkingDirectory(const std::filesystem::path&, Type type = DIRECTORY); + ~WorkingDirectory(); + }; +} \ No newline at end of file diff --git a/src/util/xml_.cpp b/src/util/xml_.cpp index ef37a06..787ce72 100644 --- a/src/util/xml_.cpp +++ b/src/util/xml_.cpp @@ -1,10 +1,10 @@ #include "xml_.h" -#include "filesystem_.h" #include "math_.h" +#include "path_.h" +using namespace anm2ed::util; using namespace tinyxml2; -namespace filesystem = anm2ed::util::filesystem; namespace anm2ed::util::xml { @@ -27,7 +27,7 @@ namespace anm2ed::util::xml { std::string temp{}; auto result = query_string_attribute(element, attribute, &temp); - if (result == XML_SUCCESS) *out = filesystem::path_from_utf8(temp); + if (result == XML_SUCCESS) *out = path::from_utf8(temp); return result; }