diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b1627fc..85bc328 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -45,6 +45,9 @@ add_executable(tomato ./file_selector.hpp ./file_selector.cpp + ./send_image_popup.hpp + ./send_image_popup.cpp + ./chat_gui4.hpp ./chat_gui4.cpp ) diff --git a/src/chat_gui4.cpp b/src/chat_gui4.cpp index 594c43f..1e0a43d 100644 --- a/src/chat_gui4.cpp +++ b/src/chat_gui4.cpp @@ -21,6 +21,9 @@ #include #include #include +#include +#include +#include namespace Components { @@ -40,7 +43,7 @@ ChatGui4::ChatGui4( RegistryMessageModel& rmm, Contact3Registry& cr, TextureUploaderI& tu -) : _conf(conf), _rmm(rmm), _cr(cr), _tal(_cr), _contact_tc(_tal, tu), _msg_tc(_mil, tu) { +) : _conf(conf), _rmm(rmm), _cr(cr), _tal(_cr), _contact_tc(_tal, tu), _msg_tc(_mil, tu), _sip(tu) { } void ChatGui4::render(void) { @@ -353,9 +356,60 @@ void ChatGui4::render(void) { [](){} ); } + + { + // TODO: add support for more than images + // !!! polling each frame can be VERY expensive !!! + //const auto* mime_type = clipboardHasImage(); + //ImGui::BeginDisabled(mime_type == nullptr); + if (ImGui::Button("paste\nfile", {-FLT_MIN, 0})) { + const auto* mime_type = clipboardHasImage(); + if (mime_type != nullptr) { // making sure + size_t data_size = 0; + void* data = SDL_GetClipboardData(mime_type, &data_size); + + std::cout << "CG: pasted image of size " << data_size << " mime " << mime_type << "\n"; + + _sip.sendMemory( + static_cast(data), data_size, + [this](const auto& img_data, const auto file_ext) { + // create file name + // TODO: move this into sip + std::ostringstream tmp_file_name {"tomato_Image_", std::ios_base::ate}; + { + const auto now = std::chrono::system_clock::now(); + const auto ctime = std::chrono::system_clock::to_time_t(now); + tmp_file_name + << std::put_time(std::localtime(&ctime), "%F_%H-%M-%S") + << "." + << std::setfill('0') << std::setw(4) + << std::chrono::duration_cast(now.time_since_epoch() - std::chrono::duration_cast(now.time_since_epoch())).count() + << file_ext + ; + } + + std::cout << "tmp image path " << tmp_file_name.str() << "\n"; + + const std::filesystem::path tmp_send_file_path = "tmp_send_files"; + std::filesystem::create_directories(tmp_send_file_path); + const auto tmp_file_path = tmp_send_file_path / tmp_file_name.str(); + + std::ofstream(tmp_file_path, std::ios_base::out | std::ios_base::binary) + .write(reinterpret_cast(img_data.data()), img_data.size()); + + _rmm.sendFilePath(*_selected_contact, tmp_file_name.str(), tmp_file_path.u8string()); + }, + [](){} + ); + SDL_free(data); // free data + } + } + //ImGui::EndDisabled(); + } } ImGui::EndChild(); +#if 0 // if preview window not open? if (ImGui::IsKeyPressed(ImGuiKey_V) && ImGui::IsKeyPressed(ImGuiMod_Shortcut, false)) { std::cout << "CG: paste?\n"; @@ -366,6 +420,7 @@ void ChatGui4::render(void) { std::cout << "CG: pasted image of size " << data_size << " mime " << mime_type << "\n"; } } +#endif } ImGui::EndChild(); } @@ -373,6 +428,7 @@ void ChatGui4::render(void) { ImGui::End(); _fss.render(); + _sip.render(); _contact_tc.workLoadQueue(); _msg_tc.workLoadQueue(); @@ -523,8 +579,25 @@ void ChatGui4::renderMessageBodyFile(Message3Registry& reg, const Message3 e) { // if has local, display save base path? for (size_t i = 0; i < file_list.size(); i++) { - // TODO: selectable text widget + ImGui::PushID(i); + + // TODO: selectable text widget ? ImGui::Bullet(); ImGui::Text("%s (%lu)", file_list[i].file_name.c_str(), file_list[i].file_size); + + if (reg.all_of(e)) { + const auto& local_info = reg.get(e); + if (local_info.file_list.size() > i && ImGui::BeginPopupContextItem("##file_c")) { + if (ImGui::MenuItem("open")) { + std::string url{"file://" + std::filesystem::canonical(local_info.file_list.at(i)).u8string()}; + std::cout << "opening file '" << url << "'\n"; + + SDL_OpenURL(url.c_str()); + } + ImGui::EndPopup(); + } + } + + ImGui::PopID(); } if (file_list.size() == 1 && reg.all_of(e)) { @@ -546,6 +619,7 @@ void ChatGui4::renderMessageBodyFile(Message3Registry& reg, const Message3 e) { } ImGui::Image(id, ImVec2{static_cast(width), static_cast(height)}); + // TODO: clickable to open in internal image viewer } } @@ -663,6 +737,9 @@ bool ChatGui4::renderContactListContactBig(const Contact3 c, const bool selected color_current ); + // TODO: move this out of chat gui + any_unread = false; + ImGui::SameLine(); ImGui::BeginGroup(); { @@ -681,6 +758,7 @@ bool ChatGui4::renderContactListContactBig(const Contact3 c, const bool selected std::cout << "\n"; #endif has_unread = true; + any_unread = true; } } diff --git a/src/chat_gui4.hpp b/src/chat_gui4.hpp index f1b8af0..3c5243a 100644 --- a/src/chat_gui4.hpp +++ b/src/chat_gui4.hpp @@ -8,6 +8,7 @@ #include "./tox_avatar_loader.hpp" #include "./message_image_loader.hpp" #include "./file_selector.hpp" +#include "./send_image_popup.hpp" #include #include @@ -23,6 +24,7 @@ class ChatGui4 { TextureCache _msg_tc; FileSelector _fss; + SendImagePopup _sip; std::optional _selected_contact; @@ -45,6 +47,9 @@ class ChatGui4 { public: void render(void); + public: + bool any_unread {false}; + private: void renderMessageBodyText(Message3Registry& reg, const Message3 e); void renderMessageBodyFile(Message3Registry& reg, const Message3 e); diff --git a/src/image_loader.hpp b/src/image_loader.hpp index 830a8db..d67ca72 100644 --- a/src/image_loader.hpp +++ b/src/image_loader.hpp @@ -9,6 +9,7 @@ struct ImageLoaderI { struct ImageInfo { uint32_t width {0}; uint32_t height {0}; + const char* file_ext {nullptr}; }; virtual ImageInfo loadInfoFromMemory(const uint8_t* data, uint64_t data_size) = 0; @@ -20,6 +21,7 @@ struct ImageLoaderI { std::vector data; }; std::vector frames; + const char* file_ext {nullptr}; }; virtual ImageResult loadFromMemoryRGBA(const uint8_t* data, uint64_t data_size) = 0; }; diff --git a/src/image_loader_sdl_bmp.cpp b/src/image_loader_sdl_bmp.cpp index 0a16b15..0f43265 100644 --- a/src/image_loader_sdl_bmp.cpp +++ b/src/image_loader_sdl_bmp.cpp @@ -16,6 +16,7 @@ ImageLoaderSDLBMP::ImageInfo ImageLoaderSDLBMP::loadInfoFromMemory(const uint8_t res.width = surf->w; res.height = surf->h; + res.file_ext = "bmp"; SDL_DestroySurface(surf); @@ -40,6 +41,7 @@ ImageLoaderSDLBMP::ImageResult ImageLoaderSDLBMP::loadFromMemoryRGBA(const uint8 res.width = surf->w; res.height = surf->h; + res.file_ext = "bmp"; SDL_LockSurface(conv_surf); diff --git a/src/image_loader_stb.cpp b/src/image_loader_stb.cpp index 167264a..5521032 100644 --- a/src/image_loader_stb.cpp +++ b/src/image_loader_stb.cpp @@ -29,6 +29,7 @@ ImageLoaderSTB::ImageResult ImageLoaderSTB::loadFromMemoryRGBA(const uint8_t* da if (img_data) { res.width = x; res.height = y; + res.file_ext = "gif"; const size_t stride = x * y * 4; diff --git a/src/image_loader_webp.cpp b/src/image_loader_webp.cpp index b0cd7a6..17bd4d8 100644 --- a/src/image_loader_webp.cpp +++ b/src/image_loader_webp.cpp @@ -27,6 +27,7 @@ ImageLoaderWebP::ImageInfo ImageLoaderWebP::loadInfoFromMemory(const uint8_t* da WebPAnimDecoderGetInfo(dec, &anim_info); res.width = anim_info.canvas_width; res.height = anim_info.canvas_height; + res.file_ext = "webp"; return res; } @@ -53,6 +54,7 @@ ImageLoaderWebP::ImageResult ImageLoaderWebP::loadFromMemoryRGBA(const uint8_t* WebPAnimDecoderGetInfo(dec, &anim_info); res.width = anim_info.canvas_width; res.height = anim_info.canvas_height; + res.file_ext = "webp"; int prev_timestamp = 0; while (WebPAnimDecoderHasMoreFrames(dec)) { diff --git a/src/send_image_popup.cpp b/src/send_image_popup.cpp new file mode 100644 index 0000000..72f9ae5 --- /dev/null +++ b/src/send_image_popup.cpp @@ -0,0 +1,166 @@ +#include "./send_image_popup.hpp" + +#include "./image_loader_sdl_bmp.hpp" +#include "./image_loader_stb.hpp" +#include "./image_loader_webp.hpp" + +#include + +SendImagePopup::SendImagePopup(TextureUploaderI& tu) : _tu(tu) { + _image_loaders.push_back(std::make_unique()); + _image_loaders.push_back(std::make_unique()); + _image_loaders.push_back(std::make_unique()); +} + +void SendImagePopup::reset(void) { + _on_send = [](const auto&, auto){}; + _on_cancel = [](){}; + + // hm + original_data.clear(); + { + original_image.width = 0; + original_image.height = 0; + original_image.frames.clear(); + } + + // clear preview img + for (const auto& tex_id : preview_image.textures) { + _tu.destroy(tex_id); + } + preview_image = {}; +} + +bool SendImagePopup::load(void) { + // try all loaders after another + for (auto& il : _image_loaders) { + original_image = il->loadFromMemoryRGBA(original_data.data(), original_data.size()); + if (original_image.frames.empty() || original_image.height == 0 || original_image.width == 0) { + continue; + } + + original_file_ext = "."; + if (original_image.file_ext != nullptr) { + original_file_ext += original_image.file_ext; + } else { + original_file_ext += "unk"; + } + + preview_image.timestamp_last_rendered = getNowMS(); + preview_image.current_texture = 0; + for (const auto& [ms, data] : original_image.frames) { + const auto n_t = _tu.uploadRGBA(data.data(), original_image.width, original_image.height); + preview_image.textures.push_back(n_t); + preview_image.frame_duration.push_back(ms); + } + + // redundant + preview_image.width = original_image.width; + preview_image.height = original_image.height; + + if (original_image.frames.size() > 1) { + std::cout << "SIP: loaded animation\n"; + } else { + std::cout << "SIP: loaded image\n"; + } + + return true; + } + + return false; +} + +void SendImagePopup::sendMemory( + const uint8_t* data, size_t data_size, + std::function&, std::string_view)>&& on_send, + std::function&& on_cancel +) { + original_raw = false; + if (data == nullptr || data_size == 0) { + return; // error + } + + // copy paste data to memory + original_data.clear(); + original_data.insert(original_data.begin(), data, data+data_size); + + if (!load()) { + std::cerr << "SIP: failed to load image from memory\n"; + reset(); + return; + } + + _open_popup = true; + + _on_send = std::move(on_send); + _on_cancel = std::move(on_cancel); + +} + +void SendImagePopup::render(void) { + if (_open_popup) { + _open_popup = false; + ImGui::OpenPopup("send image##SendImagePopup"); + + //const auto TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x; + //const auto TEXT_BASE_HEIGHT = ImGui::GetTextLineHeightWithSpacing(); + + //ImGui::SetNextWindowSize({TEXT_BASE_WIDTH*100, TEXT_BASE_HEIGHT*30}); + } + + // TODO: add cancel action + if (ImGui::BeginPopupModal("send image##SendImagePopup", nullptr/*, ImGuiWindowFlags_NoDecoration*/)) { + const auto TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x; + const auto TEXT_BASE_HEIGHT = ImGui::GetTextLineHeightWithSpacing(); + + preview_image.doAnimation(getNowMS()); + + //ImGui::Text("send file....\n......"); + + { + float width = ImGui::GetWindowContentRegionMax().x - ImGui::GetWindowContentRegionMin().x; + float height = preview_image.height * (width / preview_image.width); + + const float max_height = + ImGui::GetWindowContentRegionMax().y + - ( + ImGui::GetWindowContentRegionMin().y + + TEXT_BASE_HEIGHT*(2-1) // row of buttons (-1 bc fh inclues fontsize) + + ImGui::GetFrameHeightWithSpacing() + ) + ; + if (height > max_height) { + width *= max_height / height; + height = max_height; + } + + // TODO: propergate type + ImGui::Image( + preview_image.getID(), + ImVec2{static_cast(width), static_cast(height)} + ); + } + + if (ImGui::Button("X cancel", {ImGui::GetWindowContentRegionWidth()/2.f, TEXT_BASE_HEIGHT*2})) { + _on_cancel(); + ImGui::CloseCurrentPopup(); + reset(); + } + ImGui::SameLine(); + if (ImGui::Button("send ->", {-FLT_MIN, TEXT_BASE_HEIGHT*2})) { + //if (_is_valid(_current_file_path)) { + + // if modified + //_on_send(data); + // else + _on_send(original_data, original_file_ext); + + ImGui::CloseCurrentPopup(); + reset(); + //} + } + + ImGui::EndPopup(); + } +} + diff --git a/src/send_image_popup.hpp b/src/send_image_popup.hpp new file mode 100644 index 0000000..c95e897 --- /dev/null +++ b/src/send_image_popup.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +#include "./image_loader.hpp" +#include "./texture_cache.hpp" + +struct SendImagePopup { + TextureUploaderI& _tu; + + // private + std::vector> _image_loaders; + + // copy of the original data, dont touch! + std::vector original_data; + bool original_raw {false}; + std::string original_file_ext; // if !original_raw + + ImageLoaderI::ImageResult original_image; + + struct Rect { + uint32_t x {0}; + uint32_t y {0}; + + uint32_t w {0}; + uint32_t h {0}; + }; + Rect crop_rect; + + // texture to render (orig img) + TextureEntry preview_image; + + bool _open_popup {false}; + + std::function&, std::string_view)> _on_send = [](const auto&, auto){}; + std::function _on_cancel = [](){}; + + void reset(void); + + // loads the image in original_data + // fills in original_image, preview_image and crop_rect + // returns if loaded successfully + bool load(void); + + public: + SendImagePopup(TextureUploaderI& tu); + + void sendMemory( + const uint8_t* data, size_t data_size, + std::function&, std::string_view)>&& on_send, + std::function&& on_cancel + ); + // from memory_raw + // from file_path + + // call this each frame + void render(void); +}; +