From 82b0748830509ec50b590d4577542de19db36c76 Mon Sep 17 00:00:00 2001 From: Georgi Gerganov Date: Wed, 30 Dec 2020 12:04:38 +0200 Subject: [PATCH 1/3] ggwave-gui : wip file sharing support --- .gitmodules | 3 + examples/ggwave-common-sdl2.cpp | 12 +- examples/ggwave-common-sdl2.h | 2 + examples/ggwave-common.h | 2 - examples/ggwave-gui/CMakeLists.txt | 1 + examples/ggwave-gui/common.cpp | 454 ++++++++++++++++-- examples/ggwave-gui/common.h | 25 + examples/ggwave-gui/main.cpp | 45 +- examples/third-party/CMakeLists.txt | 1 + examples/third-party/ggsock | 1 + .../imgui/imgui-extra/imgui_impl.cpp | 17 +- .../imgui/imgui-extra/imgui_impl.h | 1 + 12 files changed, 516 insertions(+), 48 deletions(-) create mode 160000 examples/third-party/ggsock diff --git a/.gitmodules b/.gitmodules index aba14e7..ca77b6a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "examples/third-party/imgui/imgui"] path = examples/third-party/imgui/imgui url = https://github.com/ocornut/imgui +[submodule "examples/third-party/ggsock"] + path = examples/third-party/ggsock + url = https://github.com/ggerganov/ggsock diff --git a/examples/ggwave-common-sdl2.cpp b/examples/ggwave-common-sdl2.cpp index 088a2b6..6089dc9 100644 --- a/examples/ggwave-common-sdl2.cpp +++ b/examples/ggwave-common-sdl2.cpp @@ -1,10 +1,9 @@ #include "ggwave-common-sdl2.h" -#include "ggwave-common.h" - #include "ggwave/ggwave.h" -#include +#include "ggwave-common.h" + #include #include @@ -15,6 +14,8 @@ #define EMSCRIPTEN_KEEPALIVE #endif +constexpr double kBaseSampleRate = 48000.0; + namespace { std::string g_defaultCaptureDeviceName = ""; @@ -30,7 +31,6 @@ GGWave *g_ggWave = nullptr; } // JS interface - extern "C" { EMSCRIPTEN_KEEPALIVE int sendData(int textLength, const char * text, int protocolId, int volume) { @@ -117,7 +117,7 @@ bool GGWave_init( SDL_AudioSpec playbackSpec; SDL_zero(playbackSpec); - playbackSpec.freq = GGWave::kBaseSampleRate; + playbackSpec.freq = ::kBaseSampleRate; playbackSpec.format = AUDIO_S16SYS; playbackSpec.channels = 1; playbackSpec.samples = 16*1024; @@ -160,7 +160,7 @@ bool GGWave_init( if (g_devIdIn == 0) { SDL_AudioSpec captureSpec; captureSpec = g_obtainedSpecOut; - captureSpec.freq = GGWave::kBaseSampleRate; + captureSpec.freq = ::kBaseSampleRate; captureSpec.format = AUDIO_F32SYS; captureSpec.samples = 4096; diff --git a/examples/ggwave-common-sdl2.h b/examples/ggwave-common-sdl2.h index a411bd2..2232d9d 100644 --- a/examples/ggwave-common-sdl2.h +++ b/examples/ggwave-common-sdl2.h @@ -1,5 +1,7 @@ #pragma once +#include + #include class GGWave; diff --git a/examples/ggwave-common.h b/examples/ggwave-common.h index 5258b18..2b87e39 100644 --- a/examples/ggwave-common.h +++ b/examples/ggwave-common.h @@ -4,8 +4,6 @@ #include #include -// some basic helper methods for the examples - template float getTime_ms(const T & tStart, const T & tEnd) { return ((float)(std::chrono::duration_cast(tEnd - tStart).count()))/1000.0; diff --git a/examples/ggwave-gui/CMakeLists.txt b/examples/ggwave-gui/CMakeLists.txt index 0cec22e..4218dda 100644 --- a/examples/ggwave-gui/CMakeLists.txt +++ b/examples/ggwave-gui/CMakeLists.txt @@ -9,6 +9,7 @@ target_link_libraries(ggwave-gui PRIVATE ggwave ggwave-common ggwave-common-sdl2 + ggsock imgui-sdl2 ${CMAKE_THREAD_LIBS_INIT} ) diff --git a/examples/ggwave-gui/common.cpp b/examples/ggwave-gui/common.cpp index 559b23e..2240f49 100644 --- a/examples/ggwave-gui/common.cpp +++ b/examples/ggwave-gui/common.cpp @@ -4,6 +4,10 @@ #include "ggwave-common.h" +#include "ggsock/communicator.h" +#include "ggsock/file-server.h" +#include "ggsock/serialization.h" + #include #include @@ -17,6 +21,8 @@ #include #include #include +#include +#include #if defined(IOS) || defined(ANDROID) #include "imgui-wrapper/icons_font_awesome.h" @@ -29,14 +35,47 @@ #define ICON_FA_PLAY_CIRCLE "" #define ICON_FA_ARROW_CIRCLE_DOWN "V" #define ICON_FA_PASTE "P" +#define ICON_FA_FILE "" #endif +namespace { +char * toTimeString(const std::chrono::system_clock::time_point & tp) { + time_t t = std::chrono::system_clock::to_time_t(tp); + std::tm * ptm = std::localtime(&t); + static char buffer[32]; + std::strftime(buffer, 32, "%H:%M:%S", ptm); + return buffer; +} + +void ScrollWhenDraggingOnVoid(const ImVec2& delta, ImGuiMouseButton mouse_button) { + ImGuiContext& g = *ImGui::GetCurrentContext(); + ImGuiWindow* window = g.CurrentWindow; + bool hovered = false; + bool held = false; + ImGuiButtonFlags button_flags = (mouse_button == 0) ? ImGuiButtonFlags_MouseButtonLeft : (mouse_button == 1) ? ImGuiButtonFlags_MouseButtonRight : ImGuiButtonFlags_MouseButtonMiddle; + if (g.HoveredId == 0) // If nothing hovered so far in the frame (not same as IsAnyItemHovered()!) + ImGui::ButtonBehavior(window->Rect(), window->GetID("##scrolldraggingoverlay"), &hovered, &held, button_flags); + if (held && delta.x != 0.0f) + ImGui::SetScrollX(window, window->Scroll.x + delta.x); + if (held && delta.y != 0.0f) + ImGui::SetScrollY(window, window->Scroll.y + delta.y); +} +} + +static const char * kFileBroadcastPrefix = "\xbc"; + struct Message { + enum Type { + Text, + FileBroadcast, + }; + bool received; std::chrono::system_clock::time_point timestamp; std::string data; int protocolId; float volume; + Type type; }; struct GGWaveStats { @@ -112,36 +151,183 @@ struct Buffer { Input inputUI; }; -char * toTimeString(const std::chrono::system_clock::time_point & tp) { - time_t t = std::chrono::system_clock::to_time_t(tp); - std::tm * ptm = std::localtime(&t); - static char buffer[32]; - std::strftime(buffer, 32, "%H:%M:%S", ptm); - return buffer; -} - -void ScrollWhenDraggingOnVoid(const ImVec2& delta, ImGuiMouseButton mouse_button) { - ImGuiContext& g = *ImGui::GetCurrentContext(); - ImGuiWindow* window = g.CurrentWindow; - bool hovered = false; - bool held = false; - ImGuiButtonFlags button_flags = (mouse_button == 0) ? ImGuiButtonFlags_MouseButtonLeft : (mouse_button == 1) ? ImGuiButtonFlags_MouseButtonRight : ImGuiButtonFlags_MouseButtonMiddle; - if (g.HoveredId == 0) // If nothing hovered so far in the frame (not same as IsAnyItemHovered()!) - ImGui::ButtonBehavior(window->Rect(), window->GetID("##scrolldraggingoverlay"), &hovered, &held, button_flags); - if (held && delta.x != 0.0f) - ImGui::SetScrollX(window, window->Scroll.x + delta.x); - if (held && delta.y != 0.0f) - ImGui::SetScrollY(window, window->Scroll.y + delta.y); -} - GGWave * g_ggWave; Buffer g_buffer; std::atomic g_isRunning; +bool g_focusFileSend = false; +GGSock::FileServer g_fileServer; + +bool g_hasFileInfos = false; +bool g_hasRequestedFiles = false; +GGSock::FileServer::TFileInfos g_fileInfos; +std::map g_receivedFiles; + +int g_remotePort = 23045; +std::string g_remoteIP = "127.0.0.1"; + +GGSock::Communicator g_fileClient(false); + +int g_shareId = 0; +std::string g_shareFilename; + +int g_deleteId = 0; +std::string g_deleteFilename; + +int g_receivedId = 0; + +int getShareId() { + return g_shareId; +} + +const char * getShareFilename() { + return g_shareFilename.data(); +} + +int getDeleteId() { + return g_deleteId; +} + +const char * getDeleteFilename() { + return g_deleteFilename.data(); +} + +int getReceivedId() { + return g_receivedId; +} + +std::vector getReceivedFilename() { + std::vector result; + + for (const auto & file : g_receivedFiles) { + result.push_back(file.second.info.filename.c_str()); + } + + return result; +} + +std::vector getReceivedDataBuffer() { + std::vector result; + + for (const auto & file : g_receivedFiles) { + result.push_back(file.second.data.data()); + } + + return result; +} + +std::vector getReceivedDataSize() { + std::vector result; + + for (const auto & file : g_receivedFiles) { + result.push_back(file.second.data.size()); + } + + return result; +} + +void clearFiles() { + g_fileServer.clearAllFiles(); +} + +void addFile( + const char * uri, + const char * filename, + const char * dataBuffer, + size_t dataSize) { + GGSock::FileServer::FileData file; + file.info.uri = uri; + file.info.filename = filename; + file.data.resize(dataSize); + std::memcpy(file.data.data(), dataBuffer, dataSize); + + g_fileServer.addFile(std::move(file)); + g_focusFileSend = true; +} + +void addFile( + const char * uri, + const char * filename, + std::vector && data) { + GGSock::FileServer::FileData file; + file.info.uri = uri; + file.info.filename = filename; + file.data = std::move(data); + + g_fileServer.addFile(std::move(file)); + g_focusFileSend = true; +} + +std::string generateFileBroadcastMessage() { + // todo : to binary + std::string result; + + result = kFileBroadcastPrefix; + result += ' '; + result += GGSock::Communicator::getLocalAddress(); + result += ' '; + result += std::to_string(g_fileServer.getParameters().listenPort); + result += ' '; + result += std::to_string(rand()%32000); // todo : generated key should be used to authorize incoming messages + + return result; +} + +bool isFileBroadcastMessage(const std::string & message) { + bool result = true; + + auto pSrc = kFileBroadcastPrefix; + auto pDst = message.data(); + + while (pSrc != 0) { + if (*pDst == 0 || *pSrc++ != *pDst++) { + result = false; + break; + } + } + + return result; +} + std::thread initMain() { g_isRunning = true; g_ggWave = GGWave_instance(); + g_fileServer.init({}); + + g_fileClient.setErrorCallback([](GGSock::Communicator::TErrorCode code) { + printf("Disconnected with code = %d\n", code); + }); + + g_fileClient.setMessageCallback(GGSock::FileServer::MsgFileInfosResponse, [&](const char * dataBuffer, size_t dataSize) { + printf("Received message %d, size = %d\n", GGSock::FileServer::MsgFileInfosResponse, (int) dataSize); + + size_t offset = 0; + GGSock::Unserialize()(g_fileInfos, dataBuffer, dataSize, offset); + + for (const auto & info : g_fileInfos) { + printf(" - %s : %s (size = %d, chunks = %d)\n", info.second.uri.c_str(), info.second.filename.c_str(), (int) info.second.filesize, (int) info.second.nChunks); + g_receivedFiles[info.second.uri].info = info.second; + g_receivedFiles[info.second.uri].data.resize(info.second.filesize); + } + + g_hasFileInfos = true; + + return 0; + }); + + g_fileClient.setMessageCallback(GGSock::FileServer::MsgFileChunkResponse, [&](const char * dataBuffer, size_t dataSize) { + GGSock::FileServer::FileChunkResponseData data; + + size_t offset = 0; + GGSock::Unserialize()(data, dataBuffer, dataSize, offset); + + //printf("Received chunk %d for file '%s', size = %d\n", data.chunkId, data.uri.c_str(), (int) data.data.size()); + std::memcpy(g_receivedFiles[data.uri].data.data() + data.pStart, data.data.data(), data.pLen); + + return 0; + }); + return std::thread([&]() { Input inputCurrent; @@ -171,14 +357,16 @@ std::thread initMain() { lastRxDataLength = g_ggWave->takeRxData(lastRxData); if (lastRxDataLength > 0) { + auto message = std::string((char *) lastRxData.data(), lastRxDataLength); g_buffer.stateCore.update = true; g_buffer.stateCore.flags.newMessage = true; g_buffer.stateCore.message = { true, std::chrono::system_clock::now(), - std::string((char *) lastRxData.data(), lastRxDataLength), + std::move(message), g_ggWave->getRxProtocolId(), 0, + isFileBroadcastMessage(message) ? Message::FileBroadcast : Message::Text, }; } @@ -214,6 +402,34 @@ std::thread initMain() { } void renderMain() { + g_fileServer.update(); + + if (g_fileClient.isConnected()) { + if (!g_hasFileInfos) { + g_fileClient.send(GGSock::FileServer::MsgFileInfosRequest); + } else if (g_hasRequestedFiles == false) { + printf("Requesting files ...\n"); + for (const auto & fileInfo : g_fileInfos) { + for (int i = 0; i < fileInfo.second.nChunks; ++i) { + GGSock::FileServer::FileChunkRequestData data; + data.uri = fileInfo.second.uri; + data.chunkId = i; + data.nChunksHave = 0; + data.nChunksExpected = fileInfo.second.nChunks; + + GGSock::SerializationBuffer buffer; + GGSock::Serialize()(data, buffer); + g_fileClient.send(GGSock::FileServer::MsgFileChunkRequest, buffer.data(), (int32_t) buffer.size()); + g_fileClient.update(); + } + } + g_hasRequestedFiles = true; + g_receivedId++; + } + } + + g_fileClient.update(); + static State stateCurrent; { @@ -224,15 +440,23 @@ void renderMain() { enum class WindowId { Settings, Messages, + Files, Spectrum, }; + enum SubWindowId { + Send, + Receive, + }; + struct Settings { int protocolId = 1; float volume = 0.10f; }; static WindowId windowId = WindowId::Messages; + static SubWindowId subWindowId = SubWindowId::Send; + static Settings settings; const double tHoldContextPopup = 0.5f; @@ -290,6 +514,12 @@ void renderMain() { stateCurrent.update = false; } + if (g_focusFileSend) { + windowId = WindowId::Files; + subWindowId = SubWindowId::Send; + g_focusFileSend = false; + } + if (lastMouseButtonLeft == 0 && ImGui::GetIO().MouseDown[0] == 1) { ImGui::GetIO().MouseDelta = { 0.0, 0.0 }; } @@ -307,6 +537,8 @@ void renderMain() { #endif const float menuButtonHeight = 24.0f + 2.0f*style.ItemSpacing.y; + const auto & mouse_delta = ImGui::GetIO().MouseDelta; + ImGui::SetNextWindowPos({ 0, 0, }); ImGui::SetNextWindowSize(displaySize); ImGui::Begin("Main", nullptr, @@ -322,12 +554,17 @@ void renderMain() { windowId = WindowId::Settings; } ImGui::SameLine(); - - if (ImGui::Button(ICON_FA_COMMENT_ALT " Messages", { 0.5f*ImGui::GetContentRegionAvailWidth(), menuButtonHeight })) { + + if (ImGui::Button(ICON_FA_COMMENT_ALT " Messages", { 0.35f*ImGui::GetContentRegionAvailWidth(), menuButtonHeight })) { windowId = WindowId::Messages; } ImGui::SameLine(); - + + if (ImGui::Button(ICON_FA_FILE " Files", { 0.40f*ImGui::GetContentRegionAvailWidth(), menuButtonHeight })) { + windowId = WindowId::Files; + } + ImGui::SameLine(); + if (ImGui::Button(ICON_FA_SIGNAL " Spectrum", { 1.0f*ImGui::GetContentRegionAvailWidth(), menuButtonHeight })) { windowId = WindowId::Spectrum; } @@ -336,7 +573,7 @@ void renderMain() { ImGui::BeginChild("Settings:main", ImGui::GetContentRegionAvail(), true); ImGui::Text("%s", ""); ImGui::Text("%s", ""); - ImGui::Text("Waver v1.2.0"); + ImGui::Text("Waver v1.3.0"); ImGui::Separator(); ImGui::Text("%s", ""); @@ -460,7 +697,6 @@ void renderMain() { static bool isHoldingInput = false; static int messageIdHolding = 0; - const auto & mouse_delta = ImGui::GetIO().MouseDelta; const float tMessageFlyIn = 0.3f; // we need this because we push messages in the next loop @@ -493,9 +729,16 @@ void renderMain() { p0.x -= style.ItemSpacing.x; p0.y -= 0.5f*style.ItemSpacing.y; - auto col = style.Colors[ImGuiCol_Text]; - col.w = interp; - ImGui::TextColored(col, "%s", message.data.c_str()); + if (message.type == Message::FileBroadcast) { + auto col = ImVec4 { 0.0f, 1.0f, 1.0f, 1.0f }; + col.w = interp; + ImGui::TextColored(col, "-=[ File Broadcast ]=-"); + ImGui::TextColored(col, "%s", message.data.c_str()); + } else { + auto col = style.Colors[ImGuiCol_Text]; + col.w = interp; + ImGui::TextColored(col, "%s", message.data.c_str()); + } auto p1 = ImGui::GetCursorScreenPos(); p1.x = p00.x + ImGui::GetContentRegionAvailWidth(); @@ -536,7 +779,7 @@ void renderMain() { if (ImGui::Button("Resend")) { g_buffer.inputUI.update = true; - g_buffer.inputUI.message = { false, std::chrono::system_clock::now(), messageSelected.data, messageSelected.protocolId, settings.volume }; + g_buffer.inputUI.message = { false, std::chrono::system_clock::now(), messageSelected.data, messageSelected.protocolId, settings.volume, Message::Text }; messageHistory.push_back(g_buffer.inputUI.message); ImGui::CloseCurrentPopup(); @@ -551,6 +794,26 @@ void renderMain() { ImGui::CloseCurrentPopup(); } + //if (messageSelected.received && messageSelected.type == Message::FileBroadcast) { + if (messageSelected.type == Message::FileBroadcast) { + ImGui::SameLine(); + ImGui::TextDisabled("|"); + + ImGui::SameLine(); + if (ImGui::Button("Receive")) { + std::string tmp = messageSelected.data.data() + strlen(kFileBroadcastPrefix); + std::stringstream ss(tmp); + + ss >> g_remoteIP; + ss >> g_remotePort; + + g_fileClient.disconnect(); + g_hasFileInfos = false; + g_hasRequestedFiles = false; + ImGui::CloseCurrentPopup(); + } + } + ImGui::EndPopup(); } @@ -692,7 +955,7 @@ void renderMain() { ImGui::SameLine(); if ((ImGui::Button(sendButtonText) || doSendMessage) && inputBuf[0] != 0) { g_buffer.inputUI.update = true; - g_buffer.inputUI.message = { false, std::chrono::system_clock::now(), std::string(inputBuf), settings.protocolId, settings.volume }; + g_buffer.inputUI.message = { false, std::chrono::system_clock::now(), std::string(inputBuf), settings.protocolId, settings.volume, Message::Text }; messageHistory.push_back(g_buffer.inputUI.message); @@ -707,6 +970,131 @@ void renderMain() { } } + if (windowId == WindowId::Files) { + const float subWindowButtonHeight = menuButtonHeight; + + if (ImGui::Button("Send", { 0.50f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { + subWindowId = SubWindowId::Send; + } + ImGui::SameLine(); + + if (ImGui::Button("Receive", { 1.0f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { + subWindowId = SubWindowId::Receive; + } + + switch (subWindowId) { + case SubWindowId::Send: + { + { + const auto wSize = ImVec2 { ImGui::GetContentRegionAvailWidth(), 0.60f*ImGui::GetContentRegionAvail().y }; + + ImGui::BeginChild("Files:Send:fileInfos", wSize, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + + //ImGui::PushTextWrapPos(); + auto fileInfos = g_fileServer.getFileInfos(); + for (const auto & fileInfo : fileInfos) { + ImGui::PushID(fileInfo.first); + ImGui::Text("File: '%s' (%4.2f MB)\n", fileInfo.second.filename.c_str(), float(fileInfo.second.filesize)/1024.0f/1024.0f); + if (ImGui::Button("Save")) { + g_shareFilename = fileInfo.second.filename; + g_shareId++; + } + ImGui::SameLine(); + + if (ImGui::Button("Clear")) { + g_deleteFilename = fileInfo.second.filename; + g_deleteId++; + } + + ImGui::PopID(); + } + //ImGui::PopTextWrapPos(); + + ScrollWhenDraggingOnVoid(ImVec2(-mouse_delta.x, -mouse_delta.y), ImGuiMouseButton_Left); + ImGui::EndChild(); + + if (ImGui::Button("Broadcast", { 0.40f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { + g_buffer.inputUI.update = true; + g_buffer.inputUI.message = { + false, + std::chrono::system_clock::now(), + ::generateFileBroadcastMessage(), + settings.protocolId, + settings.volume, + Message::FileBroadcast + }; + + messageHistory.push_back(g_buffer.inputUI.message); + + g_fileServer.startListening(); + } + ImGui::SameLine(); + + if (ImGui::Button("Stop", { 0.50f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { + g_fileServer.stopListening(); + } + ImGui::SameLine(); + + if (ImGui::Button("Clear", { 1.0f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { + g_deleteFilename = "###ALL-FILES###"; + g_deleteId++; + } + } + + { + const auto wSize = ImVec2 { ImGui::GetContentRegionAvailWidth(), ImGui::GetContentRegionAvail().y }; + ImGui::BeginChild("Files:Send:clientInfos", wSize, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + + if (g_fileServer.isListening() == false) { + ImGui::TextColored({ 1.0f, 1.0f, 0.0f, 1.0f }, "Not accepting new connections"); + } else { + ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Accepting new connections at %s:%d", + GGSock::Communicator::getLocalAddress().c_str(), g_fileServer.getParameters().listenPort); + } + + auto clientInfos = g_fileServer.getClientInfos(); + if (clientInfos.empty()) { + ImGui::Text("No peers currently connected .."); + } else { + for (const auto & clientInfo : clientInfos) { + ImGui::Text("Peer: %s\n", clientInfo.second.address.c_str()); + } + } + + ScrollWhenDraggingOnVoid(ImVec2(-mouse_delta.x, -mouse_delta.y), ImGuiMouseButton_Left); + ImGui::EndChild(); + } + } + break; + case SubWindowId::Receive: + { + const auto wSize = ImGui::GetContentRegionAvail(); + + ImGui::BeginChild("Files:Receive:main", wSize, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + + ImGui::Text("Remote IP: %s", g_remoteIP.c_str()); + ImGui::Text("Remote Port: %d", g_remotePort); + + if (ImGui::Button("Connect", { 0.50f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { + if (g_fileClient.connect(g_remoteIP, g_remotePort, 0)) { + printf("Started connecting ...\n"); + } + } + ImGui::SameLine(); + + if (ImGui::Button("Disconnect", { 1.00f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { + if (g_fileClient.disconnect()) { + printf("Stopped connecting\n"); + } + } + + ScrollWhenDraggingOnVoid(ImVec2(-mouse_delta.x, -mouse_delta.y), ImGuiMouseButton_Left); + ImGui::EndChild(); + } + break; + }; + } + if (windowId == WindowId::Spectrum) { ImGui::BeginChild("Spectrum:main", ImGui::GetContentRegionAvail(), true); ImGui::PushTextWrapPos(); diff --git a/examples/ggwave-gui/common.h b/examples/ggwave-gui/common.h index 78169f7..780183b 100644 --- a/examples/ggwave-gui/common.h +++ b/examples/ggwave-gui/common.h @@ -3,7 +3,32 @@ #include "ggwave-common-sdl2.h" #include +#include std::thread initMain(); void renderMain(); void deinitMain(std::thread & worker); + +int getShareId(); +const char * getShareFilename(); + +int getDeleteId(); +const char * getDeleteFilename(); + +int getReceivedId(); +std::vector getReceivedFilename(); +std::vector getReceivedDataBuffer(); +std::vector getReceivedDataSize(); + +void clearFiles(); + +void addFile( + const char * uri, + const char * filename, + const char * dataBuffer, + size_t dataSize); + +void addFile( + const char * uri, + const char * filename, + std::vector && data); diff --git a/examples/ggwave-gui/main.cpp b/examples/ggwave-gui/main.cpp index e1264ef..032e90d 100644 --- a/examples/ggwave-gui/main.cpp +++ b/examples/ggwave-gui/main.cpp @@ -7,10 +7,37 @@ #include #include -// ImGui helpers +#include +#include +#include -bool ImGui_BeginFrame(SDL_Window * window); -bool ImGui_EndFrame(SDL_Window * window); +std::vector readFile(const char* filename) { + // open the file: + std::ifstream file(filename, std::ios::binary); + + // Stop eating new lines in binary mode!!! + file.unsetf(std::ios::skipws); + + // get its size: + std::streampos fileSize; + + file.seekg(0, std::ios::end); + fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + // reserve capacity + std::vector vec; + vec.reserve(fileSize); + + // read the data: + vec.insert(vec.begin(), + std::istream_iterator(file), + std::istream_iterator()); + + return vec; +} + +// ImGui helpers bool ImGui_BeginFrame(SDL_Window * window) { SDL_Event event; @@ -19,6 +46,12 @@ bool ImGui_BeginFrame(SDL_Window * window) { ImGui_ProcessEvent(&event); if (event.type == SDL_QUIT) return false; if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_CLOSE && event.window.windowID == SDL_GetWindowID(window)) return false; + if (event.type == SDL_DROPFILE) { + printf("Dropped file: '%s'\n", event.drop.file); + auto data = readFile(event.drop.file); + addFile(event.drop.file, event.drop.file, std::move(data)); + break; + } } ImGui_NewFrame(window); @@ -123,20 +156,24 @@ int main(int argc, char** argv) { return -1; } + ImGui_PreInit(); + int windowX = 400; int windowY = 600; const char * windowTitle = "ggwave-gui"; SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); SDL_Window * window = SDL_CreateWindow(windowTitle, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, windowX, windowY, window_flags); - void * gl_context = SDL_GL_CreateContext(window); + SDL_GL_MakeCurrent(window, gl_context); SDL_GL_SetSwapInterval(1); // Enable vsync ImGui_Init(window, gl_context); ImGui_SetStyle(); + SDL_EventState(SDL_DROPFILE, SDL_ENABLE); + ImGui_NewFrame(window); ImGui::Render(); diff --git a/examples/third-party/CMakeLists.txt b/examples/third-party/CMakeLists.txt index db51b52..6fb6209 100644 --- a/examples/third-party/CMakeLists.txt +++ b/examples/third-party/CMakeLists.txt @@ -2,3 +2,4 @@ if (NOT EMSCRIPTEN) endif() add_subdirectory(imtui) add_subdirectory(imgui) +add_subdirectory(ggsock) diff --git a/examples/third-party/ggsock b/examples/third-party/ggsock new file mode 160000 index 0000000..cf34042 --- /dev/null +++ b/examples/third-party/ggsock @@ -0,0 +1 @@ +Subproject commit cf340425dcde6a67bc89d801e36edb1d4b3fc1b5 diff --git a/examples/third-party/imgui/imgui-extra/imgui_impl.cpp b/examples/third-party/imgui/imgui-extra/imgui_impl.cpp index 72d971e..bc94c39 100644 --- a/examples/third-party/imgui/imgui-extra/imgui_impl.cpp +++ b/examples/third-party/imgui/imgui-extra/imgui_impl.cpp @@ -17,18 +17,16 @@ #include -ImGuiContext* ImGui_Init(SDL_Window* window, SDL_GLContext gl_context) { +bool ImGui_PreInit() { // Decide GL+GLSL versions #if __APPLE__ // GL 3.2 Core + GLSL 150 - const char* glsl_version = "#version 150"; SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); // Always required on Mac SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); #else // GL 3.0 + GLSL 130 - const char* glsl_version = "#version 130"; SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); @@ -40,6 +38,19 @@ ImGuiContext* ImGui_Init(SDL_Window* window, SDL_GLContext gl_context) { SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); + return true; +} + +ImGuiContext* ImGui_Init(SDL_Window* window, SDL_GLContext gl_context) { + // Decide GL+GLSL versions +#if __APPLE__ + // GL 3.2 Core + GLSL 150 + const char* glsl_version = "#version 150"; +#else + // GL 3.0 + GLSL 130 + const char* glsl_version = "#version 130"; +#endif + static bool isInitialized = false; if (!isInitialized) { // Initialize OpenGL loader diff --git a/examples/third-party/imgui/imgui-extra/imgui_impl.h b/examples/third-party/imgui/imgui-extra/imgui_impl.h index 03c5b0d..9abe3cd 100644 --- a/examples/third-party/imgui/imgui-extra/imgui_impl.h +++ b/examples/third-party/imgui/imgui-extra/imgui_impl.h @@ -14,6 +14,7 @@ struct SDL_Window; typedef void * SDL_GLContext; typedef union SDL_Event SDL_Event; +IMGUI_API bool ImGui_PreInit(); IMGUI_API ImGuiContext* ImGui_Init(SDL_Window* window, SDL_GLContext gl_context); void IMGUI_API ImGui_Shutdown(); From d620f5c15be7d31ff80e643254266855d2baa7c3 Mon Sep 17 00:00:00 2001 From: Georgi Gerganov Date: Wed, 30 Dec 2020 17:37:55 +0200 Subject: [PATCH 2/3] ggwave-gui : working file sharing --- examples/ggwave-gui/CMakeLists.txt | 2 +- examples/ggwave-gui/common.cpp | 330 +++-- examples/ggwave-gui/common.h | 39 +- examples/ggwave-gui/interface-unix.cpp | 49 + examples/ggwave-gui/interface.cpp | 60 + examples/ggwave-gui/interface.h | 42 + examples/ggwave-gui/main.cpp | 19 +- examples/pfd/COPYING | 14 + examples/pfd/pfd.h | 1704 ++++++++++++++++++++++++ examples/third-party/ggsock | 2 +- 10 files changed, 2166 insertions(+), 95 deletions(-) create mode 100644 examples/ggwave-gui/interface-unix.cpp create mode 100644 examples/ggwave-gui/interface.cpp create mode 100644 examples/ggwave-gui/interface.h create mode 100644 examples/pfd/COPYING create mode 100644 examples/pfd/pfd.h diff --git a/examples/ggwave-gui/CMakeLists.txt b/examples/ggwave-gui/CMakeLists.txt index 4218dda..b891fe2 100644 --- a/examples/ggwave-gui/CMakeLists.txt +++ b/examples/ggwave-gui/CMakeLists.txt @@ -1,4 +1,4 @@ -add_executable(ggwave-gui main.cpp common.cpp) +add_executable(ggwave-gui main.cpp common.cpp interface.cpp interface-unix.cpp) target_include_directories(ggwave-gui PRIVATE .. diff --git a/examples/ggwave-gui/common.cpp b/examples/ggwave-gui/common.cpp index 2240f49..da5c0c8 100644 --- a/examples/ggwave-gui/common.cpp +++ b/examples/ggwave-gui/common.cpp @@ -1,9 +1,9 @@ #include "common.h" -#include "ggwave/ggwave.h" - #include "ggwave-common.h" +#include "ggwave/ggwave.h" + #include "ggsock/communicator.h" #include "ggsock/file-server.h" #include "ggsock/serialization.h" @@ -63,6 +63,7 @@ void ScrollWhenDraggingOnVoid(const ImVec2& delta, ImGuiMouseButton mouse_button } static const char * kFileBroadcastPrefix = "\xbc"; +static const int kMaxSimultaneousChunkRequests = 4; struct Message { enum Type { @@ -155,24 +156,41 @@ GGWave * g_ggWave; Buffer g_buffer; std::atomic g_isRunning; +// file send data bool g_focusFileSend = false; GGSock::FileServer g_fileServer; -bool g_hasFileInfos = false; -bool g_hasRequestedFiles = false; -GGSock::FileServer::TFileInfos g_fileInfos; -std::map g_receivedFiles; +// file received data +struct FileInfoExtended { + bool isReceiving = false; + bool readyToShare = false; + bool requestToShare = false; + int nReceivedChunks = 0; + int nRequestedChunks = 0; + std::vector isChunkRequested; + std::vector isChunkReceived; +}; +bool g_hasRemoteInfo = false; int g_remotePort = 23045; std::string g_remoteIP = "127.0.0.1"; +bool g_hasReceivedFileInfos = false; +bool g_hasRequestedFileInfos = false; +bool g_hasReceivedFiles = false; +GGSock::FileServer::TFileInfos g_receivedFileInfos; +std::map g_receivedFiles; +std::map g_receivedFileInfosExtended; + GGSock::Communicator g_fileClient(false); +// external api + int g_shareId = 0; -std::string g_shareFilename; +ShareInfo g_shareInfo; int g_deleteId = 0; -std::string g_deleteFilename; +DeleteInfo g_deleteInfo; int g_receivedId = 0; @@ -180,56 +198,60 @@ int getShareId() { return g_shareId; } -const char * getShareFilename() { - return g_shareFilename.data(); +ShareInfo getShareInfo() { + return g_shareInfo; } int getDeleteId() { return g_deleteId; } -const char * getDeleteFilename() { - return g_deleteFilename.data(); +DeleteInfo getDeleteInfo() { + return g_deleteInfo; } int getReceivedId() { return g_receivedId; } -std::vector getReceivedFilename() { - std::vector result; - +std::vector getReceiveInfos() { + std::vector result; + for (const auto & file : g_receivedFiles) { - result.push_back(file.second.info.filename.c_str()); + if (g_receivedFileInfosExtended[file.second.info.uri].requestToShare == false || + g_receivedFileInfosExtended[file.second.info.uri].readyToShare == true) { + continue; + } + result.push_back({ + file.second.info.uri.c_str(), + file.second.info.filename.c_str(), + file.second.data.data(), + file.second.data.size(), + }); } return result; } -std::vector getReceivedDataBuffer() { - std::vector result; - - for (const auto & file : g_receivedFiles) { - result.push_back(file.second.data.data()); +bool confirmReceive(const char * uri) { + if (g_receivedFiles.find(uri) == g_receivedFiles.end()) { + return false; } - return result; + g_receivedFiles.erase(uri); + g_receivedFileInfosExtended[uri].readyToShare = true; + + return true; } -std::vector getReceivedDataSize() { - std::vector result; - - for (const auto & file : g_receivedFiles) { - result.push_back(file.second.data.size()); - } - - return result; -} - -void clearFiles() { +void clearAllFiles() { g_fileServer.clearAllFiles(); } +void clearFile(const char * uri) { + g_fileServer.clearFile(uri); +} + void addFile( const char * uri, const char * filename, @@ -294,24 +316,30 @@ std::thread initMain() { g_ggWave = GGWave_instance(); g_fileServer.init({}); - + g_fileClient.setErrorCallback([](GGSock::Communicator::TErrorCode code) { printf("Disconnected with code = %d\n", code); + + g_hasReceivedFileInfos = false; + g_hasRequestedFileInfos = false; + g_hasReceivedFiles = false; }); g_fileClient.setMessageCallback(GGSock::FileServer::MsgFileInfosResponse, [&](const char * dataBuffer, size_t dataSize) { printf("Received message %d, size = %d\n", GGSock::FileServer::MsgFileInfosResponse, (int) dataSize); size_t offset = 0; - GGSock::Unserialize()(g_fileInfos, dataBuffer, dataSize, offset); + GGSock::Unserialize()(g_receivedFileInfos, dataBuffer, dataSize, offset); - for (const auto & info : g_fileInfos) { + for (const auto & info : g_receivedFileInfos) { printf(" - %s : %s (size = %d, chunks = %d)\n", info.second.uri.c_str(), info.second.filename.c_str(), (int) info.second.filesize, (int) info.second.nChunks); g_receivedFiles[info.second.uri].info = info.second; g_receivedFiles[info.second.uri].data.resize(info.second.filesize); + + g_receivedFileInfosExtended[info.second.uri] = {}; } - g_hasFileInfos = true; + g_hasReceivedFileInfos = true; return 0; }); @@ -325,6 +353,10 @@ std::thread initMain() { //printf("Received chunk %d for file '%s', size = %d\n", data.chunkId, data.uri.c_str(), (int) data.data.size()); std::memcpy(g_receivedFiles[data.uri].data.data() + data.pStart, data.data.data(), data.pLen); + g_receivedFileInfosExtended[data.uri].nReceivedChunks++; + g_receivedFileInfosExtended[data.uri].nRequestedChunks--; + g_receivedFileInfosExtended[data.uri].isChunkReceived[data.chunkId] = true; + return 0; }); @@ -405,31 +437,73 @@ void renderMain() { g_fileServer.update(); if (g_fileClient.isConnected()) { - if (!g_hasFileInfos) { + if (!g_hasRequestedFileInfos) { + g_receivedFileInfos.clear(); + g_receivedFiles.clear(); + g_receivedFileInfosExtended.clear(); + g_fileClient.send(GGSock::FileServer::MsgFileInfosRequest); - } else if (g_hasRequestedFiles == false) { - printf("Requesting files ...\n"); - for (const auto & fileInfo : g_fileInfos) { - for (int i = 0; i < fileInfo.second.nChunks; ++i) { + g_hasRequestedFileInfos = true; + } else { + for (const auto & fileInfo : g_receivedFileInfos) { + const auto & uri = fileInfo.second.uri; + auto & fileInfoExtended = g_receivedFileInfosExtended[uri]; + + if (fileInfoExtended.isReceiving == false) { + continue; + } + + if (fileInfoExtended.nReceivedChunks == fileInfo.second.nChunks) { + continue; + } + + int nextChunkId = 0; + while (fileInfoExtended.nRequestedChunks < kMaxSimultaneousChunkRequests) { + if (fileInfoExtended.nReceivedChunks + fileInfoExtended.nRequestedChunks == fileInfo.second.nChunks) { + break; + } + + while (fileInfoExtended.isChunkRequested[nextChunkId] == true) { + ++nextChunkId; + } + fileInfoExtended.isChunkRequested[nextChunkId] = true; + GGSock::FileServer::FileChunkRequestData data; data.uri = fileInfo.second.uri; - data.chunkId = i; + data.chunkId = nextChunkId; data.nChunksHave = 0; data.nChunksExpected = fileInfo.second.nChunks; - + GGSock::SerializationBuffer buffer; GGSock::Serialize()(data, buffer); g_fileClient.send(GGSock::FileServer::MsgFileChunkRequest, buffer.data(), (int32_t) buffer.size()); g_fileClient.update(); + + ++fileInfoExtended.nRequestedChunks; } } - g_hasRequestedFiles = true; - g_receivedId++; + + //for (const auto & fileInfo : g_receivedFileInfos) { + // for (int i = 0; i < fileInfo.second.nChunks; ++i) { + // GGSock::FileServer::FileChunkRequestData data; + // data.uri = fileInfo.second.uri; + // data.chunkId = i; + // data.nChunksHave = 0; + // data.nChunksExpected = fileInfo.second.nChunks; + + // GGSock::SerializationBuffer buffer; + // GGSock::Serialize()(data, buffer); + // g_fileClient.send(GGSock::FileServer::MsgFileChunkRequest, buffer.data(), (int32_t) buffer.size()); + // g_fileClient.update(); + // } + //} + //g_hasReceivedFiles = true; + //g_receivedId++; } } - + g_fileClient.update(); - + static State stateCurrent; { @@ -554,17 +628,17 @@ void renderMain() { windowId = WindowId::Settings; } ImGui::SameLine(); - + if (ImGui::Button(ICON_FA_COMMENT_ALT " Messages", { 0.35f*ImGui::GetContentRegionAvailWidth(), menuButtonHeight })) { windowId = WindowId::Messages; } ImGui::SameLine(); - + if (ImGui::Button(ICON_FA_FILE " Files", { 0.40f*ImGui::GetContentRegionAvailWidth(), menuButtonHeight })) { windowId = WindowId::Files; } ImGui::SameLine(); - + if (ImGui::Button(ICON_FA_SIGNAL " Spectrum", { 1.0f*ImGui::GetContentRegionAvailWidth(), menuButtonHeight })) { windowId = WindowId::Spectrum; } @@ -803,13 +877,19 @@ void renderMain() { if (ImGui::Button("Receive")) { std::string tmp = messageSelected.data.data() + strlen(kFileBroadcastPrefix); std::stringstream ss(tmp); - + ss >> g_remoteIP; ss >> g_remotePort; - + g_hasRemoteInfo = true; + g_fileClient.disconnect(); - g_hasFileInfos = false; - g_hasRequestedFiles = false; + g_hasReceivedFileInfos = false; + g_hasRequestedFileInfos = false; + g_hasReceivedFiles = false; + + windowId = WindowId::Files; + subWindowId = SubWindowId::Receive; + ImGui::CloseCurrentPopup(); } } @@ -996,16 +1076,21 @@ void renderMain() { ImGui::PushID(fileInfo.first); ImGui::Text("File: '%s' (%4.2f MB)\n", fileInfo.second.filename.c_str(), float(fileInfo.second.filesize)/1024.0f/1024.0f); if (ImGui::Button("Save")) { - g_shareFilename = fileInfo.second.filename; + g_shareInfo.uri = fileInfo.second.uri.data(); + g_shareInfo.filename = fileInfo.second.filename.data(); + const auto & fileData = g_fileServer.getFileData(g_shareInfo.uri); + g_shareInfo.dataBuffer = fileData.data.data(); + g_shareInfo.dataSize = fileData.data.size(); g_shareId++; } ImGui::SameLine(); - + if (ImGui::Button("Clear")) { - g_deleteFilename = fileInfo.second.filename; + g_deleteInfo.uri = fileInfo.second.uri.data(); + g_deleteInfo.filename = fileInfo.second.filename.data(); g_deleteId++; } - + ImGui::PopID(); } //ImGui::PopTextWrapPos(); @@ -1029,14 +1114,15 @@ void renderMain() { g_fileServer.startListening(); } ImGui::SameLine(); - + if (ImGui::Button("Stop", { 0.50f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { g_fileServer.stopListening(); } ImGui::SameLine(); if (ImGui::Button("Clear", { 1.0f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { - g_deleteFilename = "###ALL-FILES###"; + g_deleteInfo.uri = "###ALL-FILES###"; + g_deleteInfo.filename = ""; g_deleteId++; } } @@ -1051,7 +1137,7 @@ void renderMain() { ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Accepting new connections at %s:%d", GGSock::Communicator::getLocalAddress().c_str(), g_fileServer.getParameters().listenPort); } - + auto clientInfos = g_fileServer.getClientInfos(); if (clientInfos.empty()) { ImGui::Text("No peers currently connected .."); @@ -1068,28 +1154,116 @@ void renderMain() { break; case SubWindowId::Receive: { - const auto wSize = ImGui::GetContentRegionAvail(); + const float statusWindowHeight = 2*style.ItemInnerSpacing.y + 4*ImGui::GetTextLineHeightWithSpacing(); + { + const auto wSize = ImVec2 { ImGui::GetContentRegionAvailWidth(), ImGui::GetContentRegionAvail().y - subWindowButtonHeight - statusWindowHeight - 2*style.ItemInnerSpacing.y }; - ImGui::BeginChild("Files:Receive:main", wSize, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); - - ImGui::Text("Remote IP: %s", g_remoteIP.c_str()); - ImGui::Text("Remote Port: %d", g_remotePort); - - if (ImGui::Button("Connect", { 0.50f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { - if (g_fileClient.connect(g_remoteIP, g_remotePort, 0)) { - printf("Started connecting ...\n"); - } - } - ImGui::SameLine(); - - if (ImGui::Button("Disconnect", { 1.00f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { - if (g_fileClient.disconnect()) { - printf("Stopped connecting\n"); + ImGui::BeginChild("Files:Receive:fileInfos", wSize, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + + int toErase = -1; + + auto fileInfos = g_receivedFileInfos; + for (const auto & fileInfo : fileInfos) { + ImGui::PushID(fileInfo.first); + ImGui::Text("File: '%s' (%4.2f MB)\n", fileInfo.second.filename.c_str(), float(fileInfo.second.filesize)/1024.0f/1024.0f); + + const auto & uri = fileInfo.second.uri; + auto & fileInfoExtended = g_receivedFileInfosExtended[uri]; + + const bool isReceived = fileInfo.second.nChunks == fileInfoExtended.nReceivedChunks; + + if (isReceived) { + if (fileInfoExtended.requestToShare == false) { + if (ImGui::Button("To Send")) { + fileInfoExtended.requestToShare = true; + g_receivedId++; + } + } + + if (fileInfoExtended.readyToShare) { + ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Ready to share!"); + } + } else if (fileInfoExtended.isReceiving || fileInfoExtended.nReceivedChunks > 0) { + if (fileInfoExtended.isReceiving) { + if (ImGui::Button("Pause")) { + fileInfoExtended.isReceiving = false; + } + } else { + if (ImGui::Button("Resume")) { + fileInfoExtended.isReceiving = true; + } + } + + ImGui::SameLine(); + ImGui::ProgressBar(float(fileInfoExtended.nReceivedChunks)/fileInfo.second.nChunks); + } else { + if (ImGui::Button("Receive")) { + fileInfoExtended.isReceiving = true; + fileInfoExtended.isChunkReceived.resize(fileInfo.second.nChunks); + fileInfoExtended.isChunkRequested.resize(fileInfo.second.nChunks); + } + } + + if ((fileInfoExtended.isReceiving == false || isReceived) && fileInfoExtended.requestToShare == false) { + ImGui::SameLine(); + if (ImGui::Button("Clear")) { + toErase = fileInfo.first; + } + } + + ImGui::PopID(); + } + + if (toErase != -1) { + g_receivedFiles.erase(g_receivedFileInfos[toErase].uri); + g_receivedFileInfosExtended.erase(g_receivedFileInfos[toErase].uri); + g_receivedFileInfos.erase(toErase); + } + + ImGui::EndChild(); + } + + { + const auto wSize = ImVec2 { ImGui::GetContentRegionAvailWidth(), ImGui::GetContentRegionAvail().y - subWindowButtonHeight - style.ItemInnerSpacing.y }; + ImGui::BeginChild("Files:Receive:status", wSize, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + + ImGui::PushTextWrapPos(); + if (g_hasRemoteInfo == false) { + ImGui::TextColored({ 1.0f, 0.0f, 0.0f, 1.0f }, "There is no broadcast offer selected yet."); + ImGui::TextColored({ 1.0f, 0.0f, 0.0f, 1.0f }, "Wait for a broadcast message to be received first."); + } else { + if (g_fileClient.isConnected()) { + ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Successfully connected to peer:"); + } else { + ImGui::TextColored({ 1.0f, 1.0f, 0.0f, 1.0f }, "Broadcast offer from the following peer:"); + } + ImGui::Text("Remote IP: %s", g_remoteIP.c_str()); + ImGui::Text("Remote Port: %d", g_remotePort); + if (g_fileClient.isConnecting()) { + ImGui::TextColored({ 1.0f, 1.0f, 0.0f, 1.0f }, "Attempting to connect ..."); + } } } + ImGui::PopTextWrapPos(); + ScrollWhenDraggingOnVoid(ImVec2(-mouse_delta.x, -mouse_delta.y), ImGuiMouseButton_Left); ImGui::EndChild(); + + if (g_hasRemoteInfo && g_fileClient.isConnecting() == false && g_fileClient.isConnected() == false) { + if (ImGui::Button("Connect", { 1.00f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { + g_fileClient.connect(g_remoteIP, g_remotePort, 0); + } + } + + if (g_fileClient.isConnecting() || g_fileClient.isConnected()) { + if (ImGui::Button("Disconnect", { 1.00f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { + g_fileClient.disconnect(); + g_hasReceivedFileInfos = false; + g_hasRequestedFileInfos = false; + g_hasReceivedFiles = false; + } + } } break; }; diff --git a/examples/ggwave-gui/common.h b/examples/ggwave-gui/common.h index 780183b..a28eb07 100644 --- a/examples/ggwave-gui/common.h +++ b/examples/ggwave-gui/common.h @@ -9,18 +9,45 @@ std::thread initMain(); void renderMain(); void deinitMain(std::thread & worker); +// share info + +struct ShareInfo { + const char * uri; + const char * filename; + const char * dataBuffer; + size_t dataSize; +}; + int getShareId(); -const char * getShareFilename(); +ShareInfo getShareInfo(); + +// delete file + +struct DeleteInfo { + std::string uri; + std::string filename; +}; int getDeleteId(); -const char * getDeleteFilename(); +DeleteInfo getDeleteInfo(); + +// receive + +struct ReceiveInfo { + const char * uri; + const char * filename; + const char * dataBuffer; + size_t dataSize; +}; int getReceivedId(); -std::vector getReceivedFilename(); -std::vector getReceivedDataBuffer(); -std::vector getReceivedDataSize(); +std::vector getReceiveInfos(); +bool confirmReceive(const char * uri); -void clearFiles(); +// input + +void clearAllFiles(); +void clearFile(const char * uri); void addFile( const char * uri, diff --git a/examples/ggwave-gui/interface-unix.cpp b/examples/ggwave-gui/interface-unix.cpp new file mode 100644 index 0000000..d1132ee --- /dev/null +++ b/examples/ggwave-gui/interface-unix.cpp @@ -0,0 +1,49 @@ +#include "interface.h" + +#include "pfd/pfd.h" + +#include + +void interface_addFile( + const char * , + const char * , + const char * , + size_t ) { +} + +void interface_loadAllFiles() { +} + +void interface_shareFile( + const char * , + const char * filename, + const char * dataBuffer, + size_t dataSize) { + auto f = pfd::save_file("Save file", filename, { "All Files", "*" }, pfd::opt::none); + + if (f.result().empty() == false) { + printf("Saving to: %s\n", f.result().c_str()); + std::ofstream fout(f.result(), std::ios::binary); + if (fout.is_open() && fout.good()) { + fout.write(dataBuffer, dataSize); + fout.close(); + } + } +} + +void interface_deleteFile( + const char * , + const char * ) { +} + +void interface_receiveFile( + const char * uri, + const char * filename, + const char * dataBuffer, + size_t dataSize) { + addFile(uri, filename, dataBuffer, dataSize); +} + +bool interface_needReloadFiles() { + return false; +} diff --git a/examples/ggwave-gui/interface.cpp b/examples/ggwave-gui/interface.cpp new file mode 100644 index 0000000..8d70e17 --- /dev/null +++ b/examples/ggwave-gui/interface.cpp @@ -0,0 +1,60 @@ +#include "interface.h" + +int g_lastShareId = 0; +int g_lastDeleteId = 0; +int g_lastReceivedId = 0; +int g_frameCount = 0; + +void updateMain() { + auto curShareId = getShareId(); + if (curShareId != g_lastShareId) { + auto shareInfo = getShareInfo(); + interface_shareFile( + shareInfo.uri, + shareInfo.filename, + shareInfo.dataBuffer, + shareInfo.dataSize); + + g_lastShareId = curShareId; + } + + auto curDeleteId = getDeleteId(); + if (curDeleteId != g_lastDeleteId) { + auto deleteInfo = getDeleteInfo(); + interface_deleteFile(deleteInfo.uri.c_str(), deleteInfo.filename.c_str()); + + bool isRemoveAll = std::string(deleteInfo.uri) == "###ALL-FILES###"; + + if (interface_needReloadFiles() || isRemoveAll) { + clearAllFiles(); + interface_loadAllFiles(); + } else { + clearFile(deleteInfo.uri.c_str()); + } + + g_lastDeleteId = curDeleteId; + } + + auto curReceivedId = getReceivedId(); + if (curReceivedId != g_lastReceivedId) { + auto receiveInfos = getReceiveInfos(); + + int n = (int) receiveInfos.size(); + + for (int i = 0; i < n; ++i) { + interface_receiveFile( + receiveInfos[i].uri, + receiveInfos[i].filename, + receiveInfos[i].dataBuffer, + receiveInfos[i].dataSize); + confirmReceive(receiveInfos[i].uri); + } + + if (interface_needReloadFiles()) { + clearAllFiles(); + interface_loadAllFiles(); + } + + g_lastReceivedId = curReceivedId; + } +} diff --git a/examples/ggwave-gui/interface.h b/examples/ggwave-gui/interface.h new file mode 100644 index 0000000..fd22300 --- /dev/null +++ b/examples/ggwave-gui/interface.h @@ -0,0 +1,42 @@ +#ifndef interface_h +#define interface_h + +#ifdef __cplusplus +#include "common.h" + +extern "C" { +#endif + +void interface_addFile( + const char * uri, + const char * filename, + const char * dataBuffer, + size_t dataSize); + +void interface_loadAllFiles(); + +void interface_shareFile( + const char * uri, + const char * filename, + const char * dataBuffer, + size_t dataSize); + +void interface_deleteFile( + const char * uri, + const char * filename); + +void interface_receiveFile( + const char * uri, + const char * filename, + const char * dataBuffer, + size_t dataSize); + +bool interface_needReloadFiles(); + +void updateMain(); + +#ifdef __cplusplus +} +#endif + +#endif /* interface_h */ diff --git a/examples/ggwave-gui/main.cpp b/examples/ggwave-gui/main.cpp index 032e90d..cccc32b 100644 --- a/examples/ggwave-gui/main.cpp +++ b/examples/ggwave-gui/main.cpp @@ -1,4 +1,4 @@ -#include "common.h" +#include "interface.h" #include "ggwave-common.h" @@ -12,24 +12,17 @@ #include std::vector readFile(const char* filename) { - // open the file: std::ifstream file(filename, std::ios::binary); - - // Stop eating new lines in binary mode!!! file.unsetf(std::ios::skipws); - - // get its size: std::streampos fileSize; file.seekg(0, std::ios::end); fileSize = file.tellg(); file.seekg(0, std::ios::beg); - // reserve capacity std::vector vec; vec.reserve(fileSize); - // read the data: vec.insert(vec.begin(), std::istream_iterator(file), std::istream_iterator()); @@ -49,7 +42,9 @@ bool ImGui_BeginFrame(SDL_Window * window) { if (event.type == SDL_DROPFILE) { printf("Dropped file: '%s'\n", event.drop.file); auto data = readFile(event.drop.file); - addFile(event.drop.file, event.drop.file, std::move(data)); + std::string uri = event.drop.file; + auto filename = uri.substr(uri.find_last_of("/\\") + 1); + addFile(uri.c_str(), filename.c_str(), std::move(data)); break; } } @@ -179,12 +174,18 @@ int main(int argc, char** argv) { auto worker = initMain(); + // tmp + addFile("test0.raw", "test0.raw", std::vector(1024)); + addFile("test1.jpg", "test0.jpg", std::vector(1024*1024 + 624)); + addFile("test2.mpv", "test0.mov", std::vector(1024*1024*234 + 53827)); + while (true) { if (ImGui_BeginFrame(window) == false) { break; } renderMain(); + updateMain(); ImGui_EndFrame(window); } diff --git a/examples/pfd/COPYING b/examples/pfd/COPYING new file mode 100644 index 0000000..8b014d6 --- /dev/null +++ b/examples/pfd/COPYING @@ -0,0 +1,14 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + diff --git a/examples/pfd/pfd.h b/examples/pfd/pfd.h new file mode 100644 index 0000000..d52d8cf --- /dev/null +++ b/examples/pfd/pfd.h @@ -0,0 +1,1704 @@ +// +// Portable File Dialogs +// +// Copyright © 2018—2020 Sam Hocevar +// +// This library is free software. It comes without any warranty, to +// the extent permitted by applicable law. You can redistribute it +// and/or modify it under the terms of the Do What the Fuck You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +#pragma once + +#if _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN 1 +#endif +#include +#include +#include +#include // IFileDialog +#include +#include +#include // std::async + +#elif __EMSCRIPTEN__ +#include + +#else +#ifndef _POSIX_C_SOURCE +# define _POSIX_C_SOURCE 2 // for popen() +#endif +#include // popen() +#include // std::getenv() +#include // fcntl() +#include // read(), pipe(), dup2() +#include // ::kill, std::signal +#include // waitpid() +#endif + +#include // std::string +#include // std::shared_ptr +#include // std::ostream +#include // std::map +#include // std::set +#include // std::regex +#include // std::mutex, std::this_thread +#include // std::chrono + +namespace pfd +{ + +enum class button +{ + cancel = -1, + ok, + yes, + no, + abort, + retry, + ignore, +}; + +enum class choice +{ + ok = 0, + ok_cancel, + yes_no, + yes_no_cancel, + retry_cancel, + abort_retry_ignore, +}; + +enum class icon +{ + info = 0, + warning, + error, + question, +}; + +// Additional option flags for various dialog constructors +enum class opt : uint8_t +{ + none = 0, + // For file open, allow multiselect. + multiselect = 0x1, + // For file save, force overwrite and disable the confirmation dialog. + force_overwrite = 0x2, + // For folder select, force path to be the provided argument instead + // of the last opened directory, which is the Microsoft-recommended, + // user-friendly behaviour. + force_path = 0x4, +}; + +inline opt operator |(opt a, opt b) { return opt(uint8_t(a) | uint8_t(b)); } +inline bool operator &(opt a, opt b) { return bool(uint8_t(a) & uint8_t(b)); } + +// The settings class, only exposing to the user a way to set verbose mode +// and to force a rescan of installed desktop helpers (zenity, kdialog…). +class settings +{ +public: + static bool available(); + + static void verbose(bool value); + static void rescan(); + +protected: + explicit settings(bool resync = false); + + bool check_program(std::string const &program); + + inline bool is_osascript() const; + inline bool is_zenity() const; + inline bool is_kdialog() const; + + enum class flag + { + is_scanned = 0, + is_verbose, + + has_zenity, + has_matedialog, + has_qarma, + has_kdialog, + is_vista, + + max_flag, + }; + + // Static array of flags for internal state + bool const &flags(flag in_flag) const; + + // Non-const getter for the static array of flags + bool &flags(flag in_flag); +}; + +// Internal classes, not to be used by client applications +namespace internal +{ + +// Process wait timeout, in milliseconds +static int const default_wait_timeout = 20; + +class executor +{ + friend class dialog; + +public: + // High level function to get the result of a command + std::string result(int *exit_code = nullptr); + + // High level function to abort + bool kill(); + +#if _WIN32 + void start_func(std::function const &fun); + static BOOL CALLBACK enum_windows_callback(HWND hwnd, LPARAM lParam); +#elif __EMSCRIPTEN__ + void start(int exit_code); +#else + void start_process(std::vector const &command); +#endif + + ~executor(); + +protected: + bool ready(int timeout = default_wait_timeout); + void stop(); + +private: + bool m_running = false; + std::string m_stdout; + int m_exit_code = -1; +#if _WIN32 + std::future m_future; + std::set m_windows; + std::condition_variable m_cond; + std::mutex m_mutex; + DWORD m_tid; +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something +#else + pid_t m_pid = 0; + int m_fd = -1; +#endif +}; + +class platform +{ +protected: +#if _WIN32 + // Helper class around LoadLibraryA() and GetProcAddress() with some safety + class dll + { + public: + dll(std::string const &name); + ~dll(); + + template class proc + { + public: + proc(dll const &lib, std::string const &sym) + : m_proc(reinterpret_cast(::GetProcAddress(lib.handle, sym.c_str()))) + {} + + operator bool() const { return m_proc != nullptr; } + operator T *() const { return m_proc; } + + private: + T *m_proc; + }; + + private: + HMODULE handle; + }; + + // Helper class around CoInitialize() and CoUnInitialize() + class ole32_dll : public dll + { + public: + ole32_dll(); + ~ole32_dll(); + bool is_initialized(); + + private: + HRESULT m_state; + }; + + // Helper class around CreateActCtx() and ActivateActCtx() + class new_style_context + { + public: + new_style_context(); + ~new_style_context(); + + private: + HANDLE create(); + ULONG_PTR m_cookie = 0; + }; +#endif +}; + +class dialog : protected settings, protected platform +{ +public: + bool ready(int timeout = default_wait_timeout) const; + bool kill() const; + +protected: + explicit dialog(); + + std::vector desktop_helper() const; + static std::string buttons_to_name(choice _choice); + static std::string get_icon_name(icon _icon); + + std::string powershell_quote(std::string const &str) const; + std::string osascript_quote(std::string const &str) const; + std::string shell_quote(std::string const &str) const; + + // Keep handle to executing command + std::shared_ptr m_async; +}; + +class file_dialog : public dialog +{ +protected: + enum type + { + open, + save, + folder, + }; + + file_dialog(type in_type, + std::string const &title, + std::string const &default_path = "", + std::vector const &filters = {}, + opt options = opt::none); + +protected: + std::string string_result(); + std::vector vector_result(); + +#if _WIN32 + static int CALLBACK bffcallback(HWND hwnd, UINT uMsg, LPARAM, LPARAM pData); + std::string select_folder_vista(IFileDialog *ifd, bool force_path); + + std::wstring m_wtitle; + std::wstring m_wdefault_path; + + std::vector m_vector_result; +#endif +}; + +} // namespace internal + +// +// The notify widget +// + +class notify : public internal::dialog +{ +public: + notify(std::string const &title, + std::string const &message, + icon _icon = icon::info); +}; + +// +// The message widget +// + +class message : public internal::dialog +{ +public: + message(std::string const &title, + std::string const &text, + choice _choice = choice::ok_cancel, + icon _icon = icon::info); + + button result(); + +private: + // Some extra logic to map the exit code to button number + std::map m_mappings; +}; + +// +// The open_file, save_file, and open_folder widgets +// + +class open_file : public internal::file_dialog +{ +public: + open_file(std::string const &title, + std::string const &default_path = "", + std::vector const &filters = { "All Files", "*" }, + opt options = opt::none); + +#if defined(__has_cpp_attribute) +#if __has_cpp_attribute(deprecated) + // Backwards compatibility + [[deprecated("Use pfd::opt::multiselect instead of allow_multiselect")]] +#endif +#endif + open_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool allow_multiselect); + + std::vector result(); +}; + +class save_file : public internal::file_dialog +{ +public: + save_file(std::string const &title, + std::string const &default_path = "", + std::vector const &filters = { "All Files", "*" }, + opt options = opt::none); + +#if defined(__has_cpp_attribute) +#if __has_cpp_attribute(deprecated) + // Backwards compatibility + [[deprecated("Use pfd::opt::force_overwrite instead of confirm_overwrite")]] +#endif +#endif + save_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool confirm_overwrite); + + std::string result(); +}; + +class select_folder : public internal::file_dialog +{ +public: + select_folder(std::string const &title, + std::string const &default_path = "", + opt options = opt::none); + + std::string result(); +}; + +// +// Below this are all the method implementations. You may choose to define the +// macro PFD_SKIP_IMPLEMENTATION everywhere before including this header except +// in one place. This may reduce compilation times. +// + +#if !defined PFD_SKIP_IMPLEMENTATION + +// internal free functions implementations + +namespace internal +{ + +#if _WIN32 +static inline std::wstring str2wstr(std::string const &str) +{ + int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0); + std::wstring ret(len, '\0'); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPWSTR)ret.data(), (int)ret.size()); + return ret; +} + +static inline std::string wstr2str(std::wstring const &str) +{ + int len = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), nullptr, 0, nullptr, nullptr); + std::string ret(len, '\0'); + WideCharToMultiByte(CP_UTF8, 0, str.c_str(), (int)str.size(), (LPSTR)ret.data(), (int)ret.size(), nullptr, nullptr); + return ret; +} + +static inline bool is_vista() +{ + OSVERSIONINFOEXW osvi; + memset(&osvi, 0, sizeof(osvi)); + DWORDLONG const mask = VerSetConditionMask( + VerSetConditionMask( + VerSetConditionMask( + 0, VER_MAJORVERSION, VER_GREATER_EQUAL), + VER_MINORVERSION, VER_GREATER_EQUAL), + VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL); + osvi.dwOSVersionInfoSize = sizeof(osvi); + osvi.dwMajorVersion = HIBYTE(_WIN32_WINNT_VISTA); + osvi.dwMinorVersion = LOBYTE(_WIN32_WINNT_VISTA); + osvi.wServicePackMajor = 0; + + return VerifyVersionInfoW(&osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_SERVICEPACKMAJOR, mask) != FALSE; +} +#endif + +// This is necessary until C++20 which will have std::string::ends_with() etc. + +static inline bool ends_with(std::string const &str, std::string const &suffix) +{ + return suffix.size() <= str.size() && + str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +static inline bool starts_with(std::string const &str, std::string const &prefix) +{ + return prefix.size() <= str.size() && + str.compare(0, prefix.size(), prefix) == 0; +} + +} // namespace internal + +// settings implementation + +inline settings::settings(bool resync) +{ + flags(flag::is_scanned) &= !resync; + + if (flags(flag::is_scanned)) + return; + +#if _WIN32 + flags(flag::is_vista) = internal::is_vista(); +#elif !__APPLE__ + flags(flag::has_zenity) = check_program("zenity"); + flags(flag::has_matedialog) = check_program("matedialog"); + flags(flag::has_qarma) = check_program("qarma"); + flags(flag::has_kdialog) = check_program("kdialog"); + + // If multiple helpers are available, try to default to the best one + if (flags(flag::has_zenity) && flags(flag::has_kdialog)) + { + auto desktop_name = std::getenv("XDG_SESSION_DESKTOP"); + if (desktop_name && desktop_name == std::string("gnome")) + flags(flag::has_kdialog) = false; + else if (desktop_name && desktop_name == std::string("KDE")) + flags(flag::has_zenity) = false; + } +#endif + + flags(flag::is_scanned) = true; +} + +inline bool settings::available() +{ +#if _WIN32 + return true; +#elif __APPLE__ + return true; +#else + settings tmp; + return tmp.flags(flag::has_zenity) || + tmp.flags(flag::has_matedialog) || + tmp.flags(flag::has_qarma) || + tmp.flags(flag::has_kdialog); +#endif +} + +inline void settings::verbose(bool value) +{ + settings().flags(flag::is_verbose) = value; +} + +inline void settings::rescan() +{ + settings(/* resync = */ true); +} + +// Check whether a program is present using “which”. +inline bool settings::check_program(std::string const &program) +{ +#if _WIN32 + (void)program; + return false; +#elif __EMSCRIPTEN__ + (void)program; + return false; +#else + int exit_code = -1; + internal::executor async; + async.start_process({"/bin/sh", "-c", "which " + program}); + async.result(&exit_code); + return exit_code == 0; +#endif +} + +inline bool settings::is_osascript() const +{ +#if __APPLE__ + return true; +#else + return false; +#endif +} + +inline bool settings::is_zenity() const +{ + return flags(flag::has_zenity) || + flags(flag::has_matedialog) || + flags(flag::has_qarma); +} + +inline bool settings::is_kdialog() const +{ + return flags(flag::has_kdialog); +} + +inline bool const &settings::flags(flag in_flag) const +{ + static bool flags[size_t(flag::max_flag)]; + return flags[size_t(in_flag)]; +} + +inline bool &settings::flags(flag in_flag) +{ + return const_cast(static_cast(this)->flags(in_flag)); +} + +// executor implementation + +inline std::string internal::executor::result(int *exit_code /* = nullptr */) +{ + stop(); + if (exit_code) + *exit_code = m_exit_code; + return m_stdout; +} + +inline bool internal::executor::kill() +{ +#if _WIN32 + if (m_future.valid()) + { + // Close all windows that weren’t open when we started the future + auto previous_windows = m_windows; + EnumWindows(&enum_windows_callback, (LPARAM)this); + for (auto hwnd : m_windows) + if (previous_windows.find(hwnd) == previous_windows.end()) + SendMessage(hwnd, WM_CLOSE, 0, 0); + } +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something + (void)timeout; + return false; // cannot kill +#else + ::kill(m_pid, SIGKILL); +#endif + stop(); + return true; +} + +#if _WIN32 +inline BOOL CALLBACK internal::executor::enum_windows_callback(HWND hwnd, LPARAM lParam) +{ + auto that = (executor *)lParam; + + DWORD pid; + auto tid = GetWindowThreadProcessId(hwnd, &pid); + if (tid == that->m_tid) + that->m_windows.insert(hwnd); + return TRUE; +} +#endif + +#if _WIN32 +inline void internal::executor::start_func(std::function const &fun) +{ + stop(); + + auto trampoline = [fun, this]() + { + // Save our thread id so that the caller can cancel us + m_tid = GetCurrentThreadId(); + EnumWindows(&enum_windows_callback, (LPARAM)this); + m_cond.notify_all(); + return fun(&m_exit_code); + }; + + std::unique_lock lock(m_mutex); + m_future = std::async(std::launch::async, trampoline); + m_cond.wait(lock); + m_running = true; +} + +#elif __EMSCRIPTEN__ +inline void internal::executor::start(int exit_code) +{ + m_exit_code = exit_code; +} + +#else +inline void internal::executor::start_process(std::vector const &command) +{ + stop(); + m_stdout.clear(); + m_exit_code = -1; + + int in[2], out[2]; + if (pipe(in) != 0 || pipe(out) != 0) + return; + + m_pid = fork(); + if (m_pid < 0) + return; + + close(in[m_pid ? 0 : 1]); + close(out[m_pid ? 1 : 0]); + + if (m_pid == 0) + { + dup2(in[0], STDIN_FILENO); + dup2(out[1], STDOUT_FILENO); + + // Ignore stderr so that it doesn’t pollute the console (e.g. GTK+ errors from zenity) + int fd = open("/dev/null", O_WRONLY); + dup2(fd, STDERR_FILENO); + close(fd); + + std::vector args; + std::transform(command.cbegin(), command.cend(), std::back_inserter(args), + [](std::string const &s) { return const_cast(s.c_str()); }); + args.push_back(nullptr); // null-terminate argv[] + + execvp(args[0], args.data()); + exit(1); + } + + close(in[1]); + m_fd = out[0]; + auto flags = fcntl(m_fd, F_GETFL); + fcntl(m_fd, F_SETFL, flags | O_NONBLOCK); + + m_running = true; +} +#endif + +inline internal::executor::~executor() +{ + stop(); +} + +inline bool internal::executor::ready(int timeout /* = default_wait_timeout */) +{ + if (!m_running) + return true; + +#if _WIN32 + if (m_future.valid()) + { + auto status = m_future.wait_for(std::chrono::milliseconds(timeout)); + if (status != std::future_status::ready) + { + // On Windows, we need to run the message pump. If the async + // thread uses a Windows API dialog, it may be attached to the + // main thread and waiting for messages that only we can dispatch. + MSG msg; + while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + return false; + } + + m_stdout = m_future.get(); + } +#elif __EMSCRIPTEN__ || __NX__ + // FIXME: do something + (void)timeout; +#else + char buf[BUFSIZ]; + ssize_t received = read(m_fd, buf, BUFSIZ); // Flawfinder: ignore + if (received > 0) + { + m_stdout += std::string(buf, received); + return false; + } + + // Reap child process if it is dead. It is possible that the system has already reaped it + // (this happens when the calling application handles or ignores SIG_CHLD) and results in + // waitpid() failing with ECHILD. Otherwise we assume the child is running and we sleep for + // a little while. + int status; + pid_t child = waitpid(m_pid, &status, WNOHANG); + if (child != m_pid && (child >= 0 || errno != ECHILD)) + { + // FIXME: this happens almost always at first iteration + std::this_thread::sleep_for(std::chrono::milliseconds(timeout)); + return false; + } + + close(m_fd); + m_exit_code = WEXITSTATUS(status); +#endif + + m_running = false; + return true; +} + +inline void internal::executor::stop() +{ + // Loop until the user closes the dialog + while (!ready()) + ; +} + +// dll implementation + +#if _WIN32 +inline internal::platform::dll::dll(std::string const &name) + : handle(::LoadLibraryA(name.c_str())) +{} + +inline internal::platform::dll::~dll() +{ + if (handle) + ::FreeLibrary(handle); +} +#endif // _WIN32 + +// ole32_dll implementation + +#if _WIN32 +inline internal::platform::ole32_dll::ole32_dll() + : dll("ole32.dll") +{ + // Use COINIT_MULTITHREADED because COINIT_APARTMENTTHREADED causes crashes. + // See https://github.com/samhocevar/portable-file-dialogs/issues/51 + auto coinit = proc(*this, "CoInitializeEx"); + m_state = coinit(nullptr, COINIT_MULTITHREADED); +} + +inline internal::platform::ole32_dll::~ole32_dll() +{ + if (is_initialized()) + proc(*this, "CoUninitialize")(); +} + +inline bool internal::platform::ole32_dll::is_initialized() +{ + return m_state == S_OK || m_state == S_FALSE; +} +#endif + +// new_style_context implementation + +#if _WIN32 +inline internal::platform::new_style_context::new_style_context() +{ + // Only create one activation context for the whole app lifetime. + static HANDLE hctx = create(); + + if (hctx != INVALID_HANDLE_VALUE) + ActivateActCtx(hctx, &m_cookie); +} + +inline internal::platform::new_style_context::~new_style_context() +{ + DeactivateActCtx(0, m_cookie); +} + +inline HANDLE internal::platform::new_style_context::create() +{ + // This “hack” seems to be necessary for this code to work on windows XP. + // Without it, dialogs do not show and close immediately. GetError() + // returns 0 so I don’t know what causes this. I was not able to reproduce + // this behavior on Windows 7 and 10 but just in case, let it be here for + // those versions too. + // This hack is not required if other dialogs are used (they load comdlg32 + // automatically), only if message boxes are used. + dll comdlg32("comdlg32.dll"); + + // Using approach as shown here: https://stackoverflow.com/a/10444161 + UINT len = ::GetSystemDirectoryA(nullptr, 0); + std::string sys_dir(len, '\0'); + ::GetSystemDirectoryA(&sys_dir[0], len); + + ACTCTXA act_ctx = + { + // Do not set flag ACTCTX_FLAG_SET_PROCESS_DEFAULT, since it causes a + // crash with error “default context is already set”. + sizeof(act_ctx), + ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID, + "shell32.dll", 0, 0, sys_dir.c_str(), (LPCSTR)124, + }; + + return ::CreateActCtxA(&act_ctx); +} +#endif // _WIN32 + +// dialog implementation + +inline bool internal::dialog::ready(int timeout /* = default_wait_timeout */) const +{ + return m_async->ready(timeout); +} + +inline bool internal::dialog::kill() const +{ + return m_async->kill(); +} + +inline internal::dialog::dialog() + : m_async(std::make_shared()) +{ +} + +inline std::vector internal::dialog::desktop_helper() const +{ +#if __APPLE__ + return { "osascript" }; +#else + return { flags(flag::has_zenity) ? "zenity" + : flags(flag::has_matedialog) ? "matedialog" + : flags(flag::has_qarma) ? "qarma" + : flags(flag::has_kdialog) ? "kdialog" + : "echo" }; +#endif +} + +inline std::string internal::dialog::buttons_to_name(choice _choice) +{ + switch (_choice) + { + case choice::ok_cancel: return "okcancel"; + case choice::yes_no: return "yesno"; + case choice::yes_no_cancel: return "yesnocancel"; + case choice::retry_cancel: return "retrycancel"; + case choice::abort_retry_ignore: return "abortretryignore"; + /* case choice::ok: */ default: return "ok"; + } +} + +inline std::string internal::dialog::get_icon_name(icon _icon) +{ + switch (_icon) + { + case icon::warning: return "warning"; + case icon::error: return "error"; + case icon::question: return "question"; + // Zenity wants "information" but WinForms wants "info" + /* case icon::info: */ default: +#if _WIN32 + return "info"; +#else + return "information"; +#endif + } +} + +// THis is only used for debugging purposes +inline std::ostream& operator <<(std::ostream &s, std::vector const &v) +{ + int not_first = 0; + for (auto &e : v) + s << (not_first++ ? " " : "") << e; + return s; +} + +// Properly quote a string for Powershell: replace ' or " with '' or "" +// FIXME: we should probably get rid of newlines! +// FIXME: the \" sequence seems unsafe, too! +// XXX: this is no longer used but I would like to keep it around just in case +inline std::string internal::dialog::powershell_quote(std::string const &str) const +{ + return "'" + std::regex_replace(str, std::regex("['\"]"), "$&$&") + "'"; +} + +// Properly quote a string for osascript: replace \ or " with \\ or \" +// XXX: this also used to replace ' with \' when popen was used, but it would be +// smarter to do shell_quote(osascript_quote(...)) if this is needed again. +inline std::string internal::dialog::osascript_quote(std::string const &str) const +{ + return "\"" + std::regex_replace(str, std::regex("[\\\\\"]"), "\\$&") + "\""; +} + +// Properly quote a string for the shell: just replace ' with '\'' +// XXX: this is no longer used but I would like to keep it around just in case +inline std::string internal::dialog::shell_quote(std::string const &str) const +{ + return "'" + std::regex_replace(str, std::regex("'"), "'\\''") + "'"; +} + +// file_dialog implementation + +inline internal::file_dialog::file_dialog(type in_type, + std::string const &title, + std::string const &default_path /* = "" */, + std::vector const &filters /* = {} */, + opt options /* = opt::none */) +{ +#if _WIN32 + std::string filter_list; + std::regex whitespace(" *"); + for (size_t i = 0; i + 1 < filters.size(); i += 2) + { + filter_list += filters[i] + '\0'; + filter_list += std::regex_replace(filters[i + 1], whitespace, ";") + '\0'; + } + filter_list += '\0'; + + m_async->start_func([this, in_type, title, default_path, filter_list, + options](int *exit_code) -> std::string + { + (void)exit_code; + m_wtitle = internal::str2wstr(title); + m_wdefault_path = internal::str2wstr(default_path); + auto wfilter_list = internal::str2wstr(filter_list); + + // Initialise COM. This is required for the new folder selection window, + // (see https://github.com/samhocevar/portable-file-dialogs/pull/21) + // and to avoid random crashes with GetOpenFileNameW() (see + // https://github.com/samhocevar/portable-file-dialogs/issues/51) + ole32_dll ole32; + + // Folder selection uses a different method + if (in_type == type::folder) + { + if (flags(flag::is_vista)) + { + // On Vista and higher we should be able to use IFileDialog for folder selection + IFileDialog *ifd; + HRESULT hr = dll::proc(ole32, "CoCreateInstance") + (CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&ifd)); + + // In case CoCreateInstance fails (which it should not), try legacy approach + if (SUCCEEDED(hr)) + return select_folder_vista(ifd, options & opt::force_path); + } + + BROWSEINFOW bi; + memset(&bi, 0, sizeof(bi)); + + bi.lpfn = &bffcallback; + bi.lParam = (LPARAM)this; + + if (flags(flag::is_vista)) + { + if (ole32.is_initialized()) + bi.ulFlags |= BIF_NEWDIALOGSTYLE; + bi.ulFlags |= BIF_EDITBOX; + bi.ulFlags |= BIF_STATUSTEXT; + } + + auto *list = SHBrowseForFolderW(&bi); + std::string ret; + if (list) + { + auto buffer = new wchar_t[MAX_PATH]; + SHGetPathFromIDListW(list, buffer); + dll::proc(ole32, "CoTaskMemFree")(list); + ret = internal::wstr2str(buffer); + delete[] buffer; + } + return ret; + } + + OPENFILENAMEW ofn; + memset(&ofn, 0, sizeof(ofn)); + ofn.lStructSize = sizeof(OPENFILENAMEW); + ofn.hwndOwner = GetActiveWindow(); + + ofn.lpstrFilter = wfilter_list.c_str(); + + auto woutput = std::wstring(MAX_PATH * 256, L'\0'); + ofn.lpstrFile = (LPWSTR)woutput.data(); + ofn.nMaxFile = (DWORD)woutput.size(); + if (!m_wdefault_path.empty()) + { + // If a directory was provided, use it as the initial directory. If + // a valid path was provided, use it as the initial file. Otherwise, + // let the Windows API decide. + auto path_attr = GetFileAttributesW(m_wdefault_path.c_str()); + if (path_attr != INVALID_FILE_ATTRIBUTES && (path_attr & FILE_ATTRIBUTE_DIRECTORY)) + ofn.lpstrInitialDir = m_wdefault_path.c_str(); + else if (m_wdefault_path.size() <= woutput.size()) + //second argument is size of buffer, not length of string + StringCchCopyW(ofn.lpstrFile, MAX_PATH*256+1, m_wdefault_path.c_str()); + else + { + ofn.lpstrFileTitle = (LPWSTR)m_wdefault_path.data(); + ofn.nMaxFileTitle = (DWORD)m_wdefault_path.size(); + } + } + ofn.lpstrTitle = m_wtitle.c_str(); + ofn.Flags = OFN_NOCHANGEDIR | OFN_EXPLORER; + + dll comdlg32("comdlg32.dll"); + + // Apply new visual style (required for windows XP) + new_style_context ctx; + + if (in_type == type::save) + { + if (!(options & opt::force_overwrite)) + ofn.Flags |= OFN_OVERWRITEPROMPT; + + dll::proc get_save_file_name(comdlg32, "GetSaveFileNameW"); + if (get_save_file_name(&ofn) == 0) + return ""; + return internal::wstr2str(woutput.c_str()); + } + else + { + if (options & opt::multiselect) + ofn.Flags |= OFN_ALLOWMULTISELECT; + ofn.Flags |= OFN_PATHMUSTEXIST; + + dll::proc get_open_file_name(comdlg32, "GetOpenFileNameW"); + if (get_open_file_name(&ofn) == 0) + return ""; + } + + std::string prefix; + for (wchar_t const *p = woutput.c_str(); *p; ) + { + auto filename = internal::wstr2str(p); + p += wcslen(p); + // In multiselect mode, we advance p one wchar further and + // check for another filename. If there is one and the + // prefix is empty, it means we just read the prefix. + if ((options & opt::multiselect) && *++p && prefix.empty()) + { + prefix = filename + "/"; + continue; + } + + m_vector_result.push_back(prefix + filename); + } + + return ""; + }); +#else + auto command = desktop_helper(); + + if (is_osascript()) + { + std::string script = "set ret to choose"; + switch (in_type) + { + case type::save: + script += " file name"; + break; + case type::open: default: + script += " file"; + if (options & opt::multiselect) + script += " with multiple selections allowed"; + break; + case type::folder: + script += " folder"; + break; + } + + if (default_path.size()) + script += " default location " + osascript_quote(default_path); + script += " with prompt " + osascript_quote(title); + + if (in_type == type::open) + { + // Concatenate all user-provided filter patterns + std::string patterns; + for (size_t i = 0; i < filters.size() / 2; ++i) + patterns += " " + filters[2 * i + 1]; + + // Split the pattern list to check whether "*" is in there; if it + // is, we have to disable filters because there is no mechanism in + // OS X for the user to override the filter. + std::regex sep("\\s+"); + std::string filter_list; + bool has_filter = true; + std::sregex_token_iterator iter(patterns.begin(), patterns.end(), sep, -1); + std::sregex_token_iterator end; + for ( ; iter != end; ++iter) + { + auto pat = iter->str(); + if (pat == "*" || pat == "*.*") + has_filter = false; + else if (internal::starts_with(pat, "*.")) + filter_list += (filter_list.size() == 0 ? "" : ",") + + osascript_quote(pat.substr(2, pat.size() - 2)); + } + if (has_filter && filter_list.size() > 0) + script += " of type {" + filter_list + "}"; + } + + if (in_type == type::open && (options & opt::multiselect)) + { + script += "\nset s to \"\""; + script += "\nrepeat with i in ret"; + script += "\n set s to s & (POSIX path of i) & \"\\n\""; + script += "\nend repeat"; + script += "\ncopy s to stdout"; + } + else + { + script += "\nPOSIX path of ret"; + } + + command.push_back("-e"); + command.push_back(script); + } + else if (is_zenity()) + { + command.push_back("--file-selection"); + command.push_back("--filename=" + default_path); + command.push_back("--title"); + command.push_back(title); + command.push_back("--separator=\n"); + + for (size_t i = 0; i < filters.size() / 2; ++i) + { + command.push_back("--file-filter"); + command.push_back(filters[2 * i] + "|" + filters[2 * i + 1]); + } + + if (in_type == type::save) + command.push_back("--save"); + if (in_type == type::folder) + command.push_back("--directory"); + if (!(options & opt::force_overwrite)) + command.push_back("--confirm-overwrite"); + if (options & opt::multiselect) + command.push_back("--multiple"); + } + else if (is_kdialog()) + { + switch (in_type) + { + case type::save: command.push_back("--getsavefilename"); break; + case type::open: command.push_back("--getopenfilename"); break; + case type::folder: command.push_back("--getexistingdirectory"); break; + } + if (options & opt::multiselect) + command.push_back(" --multiple"); + + command.push_back(default_path); + + std::string filter; + for (size_t i = 0; i < filters.size() / 2; ++i) + filter += (i == 0 ? "" : " | ") + filters[2 * i] + "(" + filters[2 * i + 1] + ")"; + command.push_back(filter); + + command.push_back("--title"); + command.push_back(title); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +inline std::string internal::file_dialog::string_result() +{ +#if _WIN32 + return m_async->result(); +#else + auto ret = m_async->result(); + // Strip potential trailing newline (zenity). Also strip trailing slash + // added by osascript for consistency with other backends. + while (ret.back() == '\n' || ret.back() == '/') + ret = ret.substr(0, ret.size() - 1); + return ret; +#endif +} + +inline std::vector internal::file_dialog::vector_result() +{ +#if _WIN32 + m_async->result(); + return m_vector_result; +#else + std::vector ret; + auto result = m_async->result(); + for (;;) + { + // Split result along newline characters + auto i = result.find('\n'); + if (i == 0 || i == std::string::npos) + break; + ret.push_back(result.substr(0, i)); + result = result.substr(i + 1, result.size()); + } + return ret; +#endif +} + +#if _WIN32 +// Use a static function to pass as BFFCALLBACK for legacy folder select +inline int CALLBACK internal::file_dialog::bffcallback(HWND hwnd, UINT uMsg, + LPARAM, LPARAM pData) +{ + auto inst = (file_dialog *)pData; + switch (uMsg) + { + case BFFM_INITIALIZED: + SendMessage(hwnd, BFFM_SETSELECTIONW, TRUE, (LPARAM)inst->m_wdefault_path.c_str()); + break; + } + return 0; +} + +inline std::string internal::file_dialog::select_folder_vista(IFileDialog *ifd, bool force_path) +{ + std::string result; + + IShellItem *folder; + + // Load library at runtime so app doesn't link it at load time (which will fail on windows XP) + dll shell32("shell32.dll"); + dll::proc + create_item(shell32, "SHCreateItemFromParsingName"); + + if (!create_item) + return ""; + + auto hr = create_item(m_wdefault_path.c_str(), + nullptr, + IID_PPV_ARGS(&folder)); + + // Set default folder if found. This only sets the default folder. If + // Windows has any info about the most recently selected folder, it + // will display it instead. Generally, calling SetFolder() to set the + // current directory “is not a good or expected user experience and + // should therefore be avoided”: + // https://docs.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-ifiledialog-setfolder + if (SUCCEEDED(hr)) + { + if (force_path) + ifd->SetFolder(folder); + else + ifd->SetDefaultFolder(folder); + folder->Release(); + } + + // Set the dialog title and option to select folders + ifd->SetOptions(FOS_PICKFOLDERS); + ifd->SetTitle(m_wtitle.c_str()); + + hr = ifd->Show(GetActiveWindow()); + if (SUCCEEDED(hr)) + { + IShellItem* item; + hr = ifd->GetResult(&item); + if (SUCCEEDED(hr)) + { + wchar_t* wselected = nullptr; + item->GetDisplayName(SIGDN_FILESYSPATH, &wselected); + item->Release(); + + if (wselected) + { + result = internal::wstr2str(std::wstring(wselected)); + dll::proc(ole32_dll(), "CoTaskMemFree")(wselected); + } + } + } + + ifd->Release(); + + return result; +} +#endif + +// notify implementation + +inline notify::notify(std::string const &title, + std::string const &message, + icon _icon /* = icon::info */) +{ + if (_icon == icon::question) // Not supported by notifications + _icon = icon::info; + +#if _WIN32 + // Use a static shared pointer for notify_icon so that we can delete + // it whenever we need to display a new one, and we can also wait + // until the program has finished running. + struct notify_icon_data : public NOTIFYICONDATAW + { + ~notify_icon_data() { Shell_NotifyIconW(NIM_DELETE, this); } + }; + + static std::shared_ptr nid; + + // Release the previous notification icon, if any, and allocate a new + // one. Note that std::make_shared() does value initialization, so there + // is no need to memset the structure. + nid = nullptr; + nid = std::make_shared(); + + // For XP support + nid->cbSize = NOTIFYICONDATAW_V2_SIZE; + nid->hWnd = nullptr; + nid->uID = 0; + + // Flag Description: + // - NIF_ICON The hIcon member is valid. + // - NIF_MESSAGE The uCallbackMessage member is valid. + // - NIF_TIP The szTip member is valid. + // - NIF_STATE The dwState and dwStateMask members are valid. + // - NIF_INFO Use a balloon ToolTip instead of a standard ToolTip. The szInfo, uTimeout, szInfoTitle, and dwInfoFlags members are valid. + // - NIF_GUID Reserved. + nid->uFlags = NIF_MESSAGE | NIF_ICON | NIF_INFO; + + // Flag Description + // - NIIF_ERROR An error icon. + // - NIIF_INFO An information icon. + // - NIIF_NONE No icon. + // - NIIF_WARNING A warning icon. + // - NIIF_ICON_MASK Version 6.0. Reserved. + // - NIIF_NOSOUND Version 6.0. Do not play the associated sound. Applies only to balloon ToolTips + switch (_icon) + { + case icon::warning: nid->dwInfoFlags = NIIF_WARNING; break; + case icon::error: nid->dwInfoFlags = NIIF_ERROR; break; + /* case icon::info: */ default: nid->dwInfoFlags = NIIF_INFO; break; + } + + ENUMRESNAMEPROC icon_enum_callback = [](HMODULE, LPCTSTR, LPTSTR lpName, LONG_PTR lParam) -> BOOL + { + ((NOTIFYICONDATAW *)lParam)->hIcon = ::LoadIcon(GetModuleHandle(nullptr), lpName); + return false; + }; + + nid->hIcon = ::LoadIcon(nullptr, IDI_APPLICATION); + ::EnumResourceNames(nullptr, RT_GROUP_ICON, icon_enum_callback, (LONG_PTR)nid.get()); + + nid->uTimeout = 5000; + + StringCchCopyW(nid->szInfoTitle, ARRAYSIZE(nid->szInfoTitle), internal::str2wstr(title).c_str()); + StringCchCopyW(nid->szInfo, ARRAYSIZE(nid->szInfo), internal::str2wstr(message).c_str()); + + // Display the new icon + Shell_NotifyIconW(NIM_ADD, nid.get()); +#else + auto command = desktop_helper(); + + if (is_osascript()) + { + command.push_back("-e"); + command.push_back("display notification " + osascript_quote(message) + + " with title " + osascript_quote(title)); + } + else if (is_zenity()) + { + command.push_back("--notification"); + command.push_back("--window-icon"); + command.push_back(get_icon_name(_icon)); + command.push_back("--text"); + command.push_back(title + "\n" + message); + } + else if (is_kdialog()) + { + command.push_back("--icon"); + command.push_back(get_icon_name(_icon)); + command.push_back("--title"); + command.push_back(title); + command.push_back("--passivepopup"); + command.push_back(message); + command.push_back("5"); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +// message implementation + +inline message::message(std::string const &title, + std::string const &text, + choice _choice /* = choice::ok_cancel */, + icon _icon /* = icon::info */) +{ +#if _WIN32 + UINT style = MB_TOPMOST; + switch (_icon) + { + case icon::warning: style |= MB_ICONWARNING; break; + case icon::error: style |= MB_ICONERROR; break; + case icon::question: style |= MB_ICONQUESTION; break; + /* case icon::info: */ default: style |= MB_ICONINFORMATION; break; + } + + switch (_choice) + { + case choice::ok_cancel: style |= MB_OKCANCEL; break; + case choice::yes_no: style |= MB_YESNO; break; + case choice::yes_no_cancel: style |= MB_YESNOCANCEL; break; + case choice::retry_cancel: style |= MB_RETRYCANCEL; break; + case choice::abort_retry_ignore: style |= MB_ABORTRETRYIGNORE; break; + /* case choice::ok: */ default: style |= MB_OK; break; + } + + m_mappings[IDCANCEL] = button::cancel; + m_mappings[IDOK] = button::ok; + m_mappings[IDYES] = button::yes; + m_mappings[IDNO] = button::no; + m_mappings[IDABORT] = button::abort; + m_mappings[IDRETRY] = button::retry; + m_mappings[IDIGNORE] = button::ignore; + + m_async->start_func([this, text, title, style](int* exit_code) -> std::string + { + auto wtext = internal::str2wstr(text); + auto wtitle = internal::str2wstr(title); + // Apply new visual style (required for all Windows versions) + new_style_context ctx; + *exit_code = MessageBoxW(GetActiveWindow(), wtext.c_str(), wtitle.c_str(), style); + return ""; + }); + +#elif __EMSCRIPTEN__ + std::string full_message; + switch (_icon) + { + case icon::warning: full_message = "⚠️"; break; + case icon::error: full_message = "⛔"; break; + case icon::question: full_message = "❓"; break; + /* case icon::info: */ default: full_message = "ℹ"; break; + } + + full_message += ' ' + title + "\n\n" + text; + + // This does not really start an async task; it just passes the + // EM_ASM_INT return value to a fake start() function. + m_async->start(EM_ASM_INT( + { + if ($1) + return window.confirm(UTF8ToString($0)) ? 0 : -1; + alert(UTF8ToString($0)); + return 0; + }, full_message.c_str(), _choice == choice::ok_cancel)); +#else + auto command = desktop_helper(); + + if (is_osascript()) + { + std::string script = "display dialog " + osascript_quote(text) + + " with title " + osascript_quote(title); + switch (_choice) + { + case choice::ok_cancel: + script += "buttons {\"OK\", \"Cancel\"}" + " default button \"OK\"" + " cancel button \"Cancel\""; + m_mappings[256] = button::cancel; + break; + case choice::yes_no: + script += "buttons {\"Yes\", \"No\"}" + " default button \"Yes\"" + " cancel button \"No\""; + m_mappings[256] = button::no; + break; + case choice::yes_no_cancel: + script += "buttons {\"Yes\", \"No\", \"Cancel\"}" + " default button \"Yes\"" + " cancel button \"Cancel\""; + m_mappings[256] = button::cancel; + break; + case choice::retry_cancel: + script += "buttons {\"Retry\", \"Cancel\"}" + " default button \"Retry\"" + " cancel button \"Cancel\""; + m_mappings[256] = button::cancel; + break; + case choice::abort_retry_ignore: + script += "buttons {\"Abort\", \"Retry\", \"Ignore\"}" + " default button \"Retry\"" + " cancel button \"Retry\""; + m_mappings[256] = button::cancel; + break; + case choice::ok: default: + script += "buttons {\"OK\"}" + " default button \"OK\"" + " cancel button \"OK\""; + m_mappings[256] = button::ok; + break; + } + script += " with icon "; + switch (_icon) + { + #define PFD_OSX_ICON(n) "alias ((path to library folder from system domain) as text " \ + "& \"CoreServices:CoreTypes.bundle:Contents:Resources:" n ".icns\")" + case icon::info: default: script += PFD_OSX_ICON("ToolBarInfo"); break; + case icon::warning: script += "caution"; break; + case icon::error: script += "stop"; break; + case icon::question: script += PFD_OSX_ICON("GenericQuestionMarkIcon"); break; + #undef PFD_OSX_ICON + } + + command.push_back("-e"); + command.push_back(script); + } + else if (is_zenity()) + { + switch (_choice) + { + case choice::ok_cancel: + command.insert(command.end(), { "--question", "--cancel-label=Cancel", "--ok-label=OK" }); break; + case choice::yes_no: + // Do not use standard --question because it causes “No” to return -1, + // which is inconsistent with the “Yes/No/Cancel” mode below. + command.insert(command.end(), { "--question", "--switch", "--extra-button=No", "--extra-button=Yes" }); break; + case choice::yes_no_cancel: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=No", "--extra-button=Yes" }); break; + case choice::retry_cancel: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Cancel", "--extra-button=Retry" }); break; + case choice::abort_retry_ignore: + command.insert(command.end(), { "--question", "--switch", "--extra-button=Ignore", "--extra-button=Abort", "--extra-button=Retry" }); break; + case choice::ok: + default: + switch (_icon) + { + case icon::error: command.push_back("--error"); break; + case icon::warning: command.push_back("--warning"); break; + default: command.push_back("--info"); break; + } + } + + command.insert(command.end(), { "--title", title, + "--width=300", "--height=0", // sensible defaults + "--text", text, + "--icon-name=dialog-" + get_icon_name(_icon) }); + } + else if (is_kdialog()) + { + if (_choice == choice::ok) + { + switch (_icon) + { + case icon::error: command.push_back("--error"); break; + case icon::warning: command.push_back("--sorry"); break; + default: command.push_back("--msgbox"); break; + } + } + else + { + std::string flag = "--"; + if (_icon == icon::warning || _icon == icon::error) + flag += "warning"; + flag += "yesno"; + if (_choice == choice::yes_no_cancel) + flag += "cancel"; + command.push_back(flag); + if (_choice == choice::yes_no || _choice == choice::yes_no_cancel) + { + m_mappings[0] = button::yes; + m_mappings[256] = button::no; + } + } + + command.push_back(text); + command.push_back("--title"); + command.push_back(title); + + // Must be after the above part + if (_choice == choice::ok_cancel) + command.insert(command.end(), { "--yes-label", "OK", "--no-label", "Cancel" }); + } + + if (flags(flag::is_verbose)) + std::cerr << "pfd: " << command << std::endl; + + m_async->start_process(command); +#endif +} + +inline button message::result() +{ + int exit_code; + auto ret = m_async->result(&exit_code); + // osascript will say "button returned:Cancel\n" + // and others will just say "Cancel\n" + if (exit_code < 0 || // this means cancel + internal::ends_with(ret, "Cancel\n")) + return button::cancel; + if (internal::ends_with(ret, "OK\n")) + return button::ok; + if (internal::ends_with(ret, "Yes\n")) + return button::yes; + if (internal::ends_with(ret, "No\n")) + return button::no; + if (internal::ends_with(ret, "Abort\n")) + return button::abort; + if (internal::ends_with(ret, "Retry\n")) + return button::retry; + if (internal::ends_with(ret, "Ignore\n")) + return button::ignore; + if (m_mappings.count(exit_code) != 0) + return m_mappings[exit_code]; + return exit_code == 0 ? button::ok : button::cancel; +} + +// open_file implementation + +inline open_file::open_file(std::string const &title, + std::string const &default_path /* = "" */, + std::vector const &filters /* = { "All Files", "*" } */, + opt options /* = opt::none */) + : file_dialog(type::open, title, default_path, filters, options) +{ +} + +inline open_file::open_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool allow_multiselect) + : open_file(title, default_path, filters, + (allow_multiselect ? opt::multiselect : opt::none)) +{ +} + +inline std::vector open_file::result() +{ + return vector_result(); +} + +// save_file implementation + +inline save_file::save_file(std::string const &title, + std::string const &default_path /* = "" */, + std::vector const &filters /* = { "All Files", "*" } */, + opt options /* = opt::none */) + : file_dialog(type::save, title, default_path, filters, options) +{ +} + +inline save_file::save_file(std::string const &title, + std::string const &default_path, + std::vector const &filters, + bool confirm_overwrite) + : save_file(title, default_path, filters, + (confirm_overwrite ? opt::none : opt::force_overwrite)) +{ +} + +inline std::string save_file::result() +{ + return string_result(); +} + +// select_folder implementation + +inline select_folder::select_folder(std::string const &title, + std::string const &default_path /* = "" */, + opt options /* = opt::none */) + : file_dialog(type::folder, title, default_path, {}, options) +{ +} + +inline std::string select_folder::result() +{ + return string_result(); +} + +#endif // PFD_SKIP_IMPLEMENTATION + +} // namespace pfd + diff --git a/examples/third-party/ggsock b/examples/third-party/ggsock index cf34042..13852a1 160000 --- a/examples/third-party/ggsock +++ b/examples/third-party/ggsock @@ -1 +1 @@ -Subproject commit cf340425dcde6a67bc89d801e36edb1d4b3fc1b5 +Subproject commit 13852a1cf990a596227176a5aeace055b4eed12e From ccf6c90d604263a08ce464d95eadfb9694474199 Mon Sep 17 00:00:00 2001 From: Georgi Gerganov Date: Wed, 30 Dec 2020 19:43:42 +0200 Subject: [PATCH 3/3] ggwave-gui : polishing UX --- examples/ggwave-common-sdl2.cpp | 12 +- examples/ggwave-common-sdl2.h | 2 - examples/ggwave-common.h | 2 + examples/ggwave-gui/common.cpp | 313 ++++++++++++++++++++++---------- examples/ggwave-gui/main.cpp | 11 +- examples/third-party/ggsock | 2 +- 6 files changed, 231 insertions(+), 111 deletions(-) diff --git a/examples/ggwave-common-sdl2.cpp b/examples/ggwave-common-sdl2.cpp index 6089dc9..088a2b6 100644 --- a/examples/ggwave-common-sdl2.cpp +++ b/examples/ggwave-common-sdl2.cpp @@ -1,9 +1,10 @@ #include "ggwave-common-sdl2.h" -#include "ggwave/ggwave.h" - #include "ggwave-common.h" +#include "ggwave/ggwave.h" + +#include #include #include @@ -14,8 +15,6 @@ #define EMSCRIPTEN_KEEPALIVE #endif -constexpr double kBaseSampleRate = 48000.0; - namespace { std::string g_defaultCaptureDeviceName = ""; @@ -31,6 +30,7 @@ GGWave *g_ggWave = nullptr; } // JS interface + extern "C" { EMSCRIPTEN_KEEPALIVE int sendData(int textLength, const char * text, int protocolId, int volume) { @@ -117,7 +117,7 @@ bool GGWave_init( SDL_AudioSpec playbackSpec; SDL_zero(playbackSpec); - playbackSpec.freq = ::kBaseSampleRate; + playbackSpec.freq = GGWave::kBaseSampleRate; playbackSpec.format = AUDIO_S16SYS; playbackSpec.channels = 1; playbackSpec.samples = 16*1024; @@ -160,7 +160,7 @@ bool GGWave_init( if (g_devIdIn == 0) { SDL_AudioSpec captureSpec; captureSpec = g_obtainedSpecOut; - captureSpec.freq = ::kBaseSampleRate; + captureSpec.freq = GGWave::kBaseSampleRate; captureSpec.format = AUDIO_F32SYS; captureSpec.samples = 4096; diff --git a/examples/ggwave-common-sdl2.h b/examples/ggwave-common-sdl2.h index 2232d9d..a411bd2 100644 --- a/examples/ggwave-common-sdl2.h +++ b/examples/ggwave-common-sdl2.h @@ -1,7 +1,5 @@ #pragma once -#include - #include class GGWave; diff --git a/examples/ggwave-common.h b/examples/ggwave-common.h index 2b87e39..5258b18 100644 --- a/examples/ggwave-common.h +++ b/examples/ggwave-common.h @@ -4,6 +4,8 @@ #include #include +// some basic helper methods for the examples + template float getTime_ms(const T & tStart, const T & tEnd) { return ((float)(std::chrono::duration_cast(tEnd - tStart).count()))/1000.0; diff --git a/examples/ggwave-gui/common.cpp b/examples/ggwave-gui/common.cpp index da5c0c8..cd92538 100644 --- a/examples/ggwave-gui/common.cpp +++ b/examples/ggwave-gui/common.cpp @@ -13,6 +13,7 @@ #include +#include #include #include #include @@ -60,10 +61,59 @@ void ScrollWhenDraggingOnVoid(const ImVec2& delta, ImGuiMouseButton mouse_button if (held && delta.y != 0.0f) ImGui::SetScrollY(window, window->Scroll.y + delta.y); } + } -static const char * kFileBroadcastPrefix = "\xbc"; +namespace ImGui { +bool ButtonDisabled(const char* label, const ImVec2& size = ImVec2(0, 0)) { + { + auto col = ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled); + col.x *= 0.8; + col.y *= 0.8; + col.z *= 0.8; + PushStyleColor(ImGuiCol_Button, col); + PushStyleColor(ImGuiCol_ButtonHovered, col); + PushStyleColor(ImGuiCol_ButtonActive, col); + } + { + auto col = ImGui::GetStyleColorVec4(ImGuiCol_Text); + col.x *= 0.75; + col.y *= 0.75; + col.z *= 0.75; + PushStyleColor(ImGuiCol_Text, col); + } + bool result = Button(label, size); + PopStyleColor(4); + return result; +} + +bool ButtonDisablable(const char* label, const ImVec2& size = ImVec2(0, 0), bool isDisabled = false) { + if (isDisabled) { + ButtonDisabled(label, size); + return false; + } + return Button(label, size); +} + +bool ButtonSelected(const char* label, const ImVec2& size = ImVec2(0, 0)) { + auto col = ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive); + PushStyleColor(ImGuiCol_Button, col); + PushStyleColor(ImGuiCol_ButtonHovered, col); + bool result = Button(label, size); + PopStyleColor(2); + return result; +} + +bool ButtonSelectable(const char* label, const ImVec2& size = ImVec2(0, 0), bool isSelected = false) { + if (isSelected) return ButtonSelected(label, size); + return Button(label, size); +} + +} + +static const char * kFileBroadcastPrefix = "\xba\xbc\xbb"; static const int kMaxSimultaneousChunkRequests = 4; +static const float kBroadcastTime_sec = 60.0f; struct Message { enum Type { @@ -157,7 +207,14 @@ Buffer g_buffer; std::atomic g_isRunning; // file send data +struct BroadcastInfo { + std::string ip; + int port; + int key; +}; + bool g_focusFileSend = false; +float g_tLastBroadcast = -100.0f; GGSock::FileServer g_fileServer; // file received data @@ -238,8 +295,8 @@ bool confirmReceive(const char * uri) { return false; } - g_receivedFiles.erase(uri); g_receivedFileInfosExtended[uri].readyToShare = true; + g_receivedFiles.erase(uri); return true; } @@ -264,7 +321,7 @@ void addFile( std::memcpy(file.data.data(), dataBuffer, dataSize); g_fileServer.addFile(std::move(file)); - g_focusFileSend = true; + //g_focusFileSend = true; } void addFile( @@ -277,31 +334,83 @@ void addFile( file.data = std::move(data); g_fileServer.addFile(std::move(file)); - g_focusFileSend = true; + //g_focusFileSend = true; } std::string generateFileBroadcastMessage() { // todo : to binary std::string result; - result = kFileBroadcastPrefix; - result += ' '; - result += GGSock::Communicator::getLocalAddress(); - result += ' '; - result += std::to_string(g_fileServer.getParameters().listenPort); - result += ' '; - result += std::to_string(rand()%32000); // todo : generated key should be used to authorize incoming messages + int plen = strlen(kFileBroadcastPrefix); + result.resize(plen + 4 + 2 + 2); + + char *p = &result[0]; + for (int i = 0; i < (int) plen; ++i) { + *p++ = kFileBroadcastPrefix[i]; + } + + { + auto ip = GGSock::Communicator::getLocalAddress(); + std::replace(ip.begin(), ip.end(), '.', ' '); + std::stringstream ss(ip); + + { int b; ss >> b; *p++ = b; } + { int b; ss >> b; *p++ = b; } + { int b; ss >> b; *p++ = b; } + { int b; ss >> b; *p++ = b; } + } + + { + uint16_t port = g_fileServer.getParameters().listenPort; + + { int b = port/256; *p++ = b; } + { int b = port%256; *p++ = b; } + } + + { + uint16_t key = rand()%65536; + + { int b = key/256; *p++ = b; } + { int b = key%256; *p++ = b; } + } + + return result; +} + +BroadcastInfo parseBroadcastInfo(const std::string & message) { + BroadcastInfo result; + + const uint8_t *p = (uint8_t *) message.data(); + p += strlen(kFileBroadcastPrefix); + + result.ip += std::to_string((uint8_t)(*p++)); + result.ip += '.'; + result.ip += std::to_string((uint8_t)(*p++)); + result.ip += '.'; + result.ip += std::to_string((uint8_t)(*p++)); + result.ip += '.'; + result.ip += std::to_string((uint8_t)(*p++)); + + result.port = 256*((int)(*p++)); + result.port += ((int)(*p++)); + + result.key = 256*((int)(*p++)); + result.key += ((int)(*p++)); return result; } bool isFileBroadcastMessage(const std::string & message) { + if (message.size() != strlen(kFileBroadcastPrefix) + 4 + 2 + 2) { + return false; + } + bool result = true; auto pSrc = kFileBroadcastPrefix; auto pDst = message.data(); - while (pSrc != 0) { + while (*pSrc != 0) { if (*pDst == 0 || *pSrc++ != *pDst++) { result = false; break; @@ -390,6 +499,7 @@ std::thread initMain() { lastRxDataLength = g_ggWave->takeRxData(lastRxData); if (lastRxDataLength > 0) { auto message = std::string((char *) lastRxData.data(), lastRxDataLength); + const Message::Type type = isFileBroadcastMessage(message) ? Message::FileBroadcast : Message::Text; g_buffer.stateCore.update = true; g_buffer.stateCore.flags.newMessage = true; g_buffer.stateCore.message = { @@ -398,7 +508,7 @@ std::thread initMain() { std::move(message), g_ggWave->getRxProtocolId(), 0, - isFileBroadcastMessage(message) ? Message::FileBroadcast : Message::Text, + type, }; } @@ -436,6 +546,10 @@ std::thread initMain() { void renderMain() { g_fileServer.update(); + if (ImGui::GetTime() - g_tLastBroadcast > kBroadcastTime_sec && g_fileServer.isListening()) { + g_fileServer.stopListening(); + } + if (g_fileClient.isConnected()) { if (!g_hasRequestedFileInfos) { g_receivedFileInfos.clear(); @@ -482,23 +596,6 @@ void renderMain() { ++fileInfoExtended.nRequestedChunks; } } - - //for (const auto & fileInfo : g_receivedFileInfos) { - // for (int i = 0; i < fileInfo.second.nChunks; ++i) { - // GGSock::FileServer::FileChunkRequestData data; - // data.uri = fileInfo.second.uri; - // data.chunkId = i; - // data.nChunksHave = 0; - // data.nChunksExpected = fileInfo.second.nChunks; - - // GGSock::SerializationBuffer buffer; - // GGSock::Serialize()(data, buffer); - // g_fileClient.send(GGSock::FileServer::MsgFileChunkRequest, buffer.data(), (int32_t) buffer.size()); - // g_fileClient.update(); - // } - //} - //g_hasReceivedFiles = true; - //g_receivedId++; } } @@ -624,22 +721,22 @@ void renderMain() { ImGui::InvisibleButton("StatusBar", { ImGui::GetContentRegionAvailWidth(), statusBarHeight }); - if (ImGui::Button(ICON_FA_COGS, { menuButtonHeight, menuButtonHeight } )) { + if (ImGui::ButtonSelectable(ICON_FA_COGS, { menuButtonHeight, menuButtonHeight }, windowId == WindowId::Settings )) { windowId = WindowId::Settings; } ImGui::SameLine(); - if (ImGui::Button(ICON_FA_COMMENT_ALT " Messages", { 0.35f*ImGui::GetContentRegionAvailWidth(), menuButtonHeight })) { + if (ImGui::ButtonSelectable(ICON_FA_COMMENT_ALT " Messages", { 0.35f*ImGui::GetContentRegionAvailWidth(), menuButtonHeight }, windowId == WindowId::Messages)) { windowId = WindowId::Messages; } ImGui::SameLine(); - if (ImGui::Button(ICON_FA_FILE " Files", { 0.40f*ImGui::GetContentRegionAvailWidth(), menuButtonHeight })) { + if (ImGui::ButtonSelectable(ICON_FA_FILE " Files", { 0.40f*ImGui::GetContentRegionAvailWidth(), menuButtonHeight }, windowId == WindowId::Files)) { windowId = WindowId::Files; } ImGui::SameLine(); - if (ImGui::Button(ICON_FA_SIGNAL " Spectrum", { 1.0f*ImGui::GetContentRegionAvailWidth(), menuButtonHeight })) { + if (ImGui::ButtonSelectable(ICON_FA_SIGNAL " Spectrum", { 1.0f*ImGui::GetContentRegionAvailWidth(), menuButtonHeight }, windowId == WindowId::Spectrum)) { windowId = WindowId::Spectrum; } @@ -806,8 +903,8 @@ void renderMain() { if (message.type == Message::FileBroadcast) { auto col = ImVec4 { 0.0f, 1.0f, 1.0f, 1.0f }; col.w = interp; - ImGui::TextColored(col, "-=[ File Broadcast ]=-"); - ImGui::TextColored(col, "%s", message.data.c_str()); + auto broadcastInfo = parseBroadcastInfo(message.data); + ImGui::TextColored(col, "-=[ File Broadcast from %s:%d ]=-", broadcastInfo.ip.c_str(), broadcastInfo.port); } else { auto col = style.Colors[ImGuiCol_Text]; col.w = interp; @@ -851,7 +948,7 @@ void renderMain() { if (ImGui::BeginPopup("Message options")) { const auto & messageSelected = messageHistory[messageIdHolding]; - if (ImGui::Button("Resend")) { + if (ImGui::ButtonDisablable("Resend", {}, messageSelected.type != Message::Text)) { g_buffer.inputUI.update = true; g_buffer.inputUI.message = { false, std::chrono::system_clock::now(), messageSelected.data, messageSelected.protocolId, settings.volume, Message::Text }; @@ -863,23 +960,21 @@ void renderMain() { ImGui::TextDisabled("|"); ImGui::SameLine(); - if (ImGui::Button("Copy")) { + if (ImGui::ButtonDisablable("Copy", {}, messageSelected.type != Message::Text)) { SDL_SetClipboardText(messageSelected.data.c_str()); ImGui::CloseCurrentPopup(); } - //if (messageSelected.received && messageSelected.type == Message::FileBroadcast) { if (messageSelected.type == Message::FileBroadcast) { ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); - if (ImGui::Button("Receive")) { - std::string tmp = messageSelected.data.data() + strlen(kFileBroadcastPrefix); - std::stringstream ss(tmp); + if (ImGui::ButtonDisablable("Receive", {}, !messageSelected.received || messageSelected.type != Message::FileBroadcast)) { + auto broadcastInfo = parseBroadcastInfo(messageSelected.data); - ss >> g_remoteIP; - ss >> g_remotePort; + g_remoteIP = broadcastInfo.ip; + g_remotePort = broadcastInfo.port; g_hasRemoteInfo = true; g_fileClient.disconnect(); @@ -1053,26 +1148,29 @@ void renderMain() { if (windowId == WindowId::Files) { const float subWindowButtonHeight = menuButtonHeight; - if (ImGui::Button("Send", { 0.50f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { + if (ImGui::ButtonSelectable("Send", { 0.50f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight }, subWindowId == SubWindowId::Send)) { subWindowId = SubWindowId::Send; } ImGui::SameLine(); - if (ImGui::Button("Receive", { 1.0f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { + if (ImGui::ButtonSelectable("Receive", { 1.0f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight }, subWindowId == SubWindowId::Receive)) { subWindowId = SubWindowId::Receive; } switch (subWindowId) { case SubWindowId::Send: { + const float statusWindowHeight = 2*style.ItemInnerSpacing.y + 4*ImGui::GetTextLineHeightWithSpacing(); + + bool hasAtLeastOneFile = false; { - const auto wSize = ImVec2 { ImGui::GetContentRegionAvailWidth(), 0.60f*ImGui::GetContentRegionAvail().y }; + const auto wSize = ImVec2 { ImGui::GetContentRegionAvailWidth(), ImGui::GetContentRegionAvail().y - subWindowButtonHeight - statusWindowHeight - 2*style.ItemInnerSpacing.y }; ImGui::BeginChild("Files:Send:fileInfos", wSize, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); - //ImGui::PushTextWrapPos(); auto fileInfos = g_fileServer.getFileInfos(); for (const auto & fileInfo : fileInfos) { + hasAtLeastOneFile = true; ImGui::PushID(fileInfo.first); ImGui::Text("File: '%s' (%4.2f MB)\n", fileInfo.second.filename.c_str(), float(fileInfo.second.filesize)/1024.0f/1024.0f); if (ImGui::Button("Save")) { @@ -1093,49 +1191,32 @@ void renderMain() { ImGui::PopID(); } - //ImGui::PopTextWrapPos(); + + ImGui::PushTextWrapPos(); + if (hasAtLeastOneFile == false) { + ImGui::TextColored({ 1.0f, 1.0f, 0.0f, 1.0f }, "There are currently no files availble to share."); +#if defined(IOS) || defined(ANDROID) + ImGui::TextColored({ 1.0f, 1.0f, 0.0f, 1.0f }, "Share some files with this app to be able to broadcast them to nearby devices through sound."); +#else + ImGui::TextColored({ 1.0f, 1.0f, 0.0f, 1.0f }, "Drag and drop some files on this window to be able to broadcast them to nearby devices through sound."); +#endif + } + ImGui::PopTextWrapPos(); ScrollWhenDraggingOnVoid(ImVec2(-mouse_delta.x, -mouse_delta.y), ImGuiMouseButton_Left); ImGui::EndChild(); - - if (ImGui::Button("Broadcast", { 0.40f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { - g_buffer.inputUI.update = true; - g_buffer.inputUI.message = { - false, - std::chrono::system_clock::now(), - ::generateFileBroadcastMessage(), - settings.protocolId, - settings.volume, - Message::FileBroadcast - }; - - messageHistory.push_back(g_buffer.inputUI.message); - - g_fileServer.startListening(); - } - ImGui::SameLine(); - - if (ImGui::Button("Stop", { 0.50f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { - g_fileServer.stopListening(); - } - ImGui::SameLine(); - - if (ImGui::Button("Clear", { 1.0f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { - g_deleteInfo.uri = "###ALL-FILES###"; - g_deleteInfo.filename = ""; - g_deleteId++; - } } { - const auto wSize = ImVec2 { ImGui::GetContentRegionAvailWidth(), ImGui::GetContentRegionAvail().y }; + const auto wSize = ImVec2 { ImGui::GetContentRegionAvailWidth(), ImGui::GetContentRegionAvail().y - subWindowButtonHeight - style.ItemInnerSpacing.y }; + ImGui::BeginChild("Files:Send:clientInfos", wSize, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); if (g_fileServer.isListening() == false) { - ImGui::TextColored({ 1.0f, 1.0f, 0.0f, 1.0f }, "Not accepting new connections"); + ImGui::TextColored({ 1.0f, 1.0f, 0.0f, 1.0f }, "Not accepting new connections."); } else { - ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Accepting new connections at %s:%d", - GGSock::Communicator::getLocalAddress().c_str(), g_fileServer.getParameters().listenPort); + ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Accepting new connections at: %s:%d (%4.1f sec)", + GGSock::Communicator::getLocalAddress().c_str(), g_fileServer.getParameters().listenPort, kBroadcastTime_sec - ImGui::GetTime() + g_tLastBroadcast); } auto clientInfos = g_fileServer.getClientInfos(); @@ -1150,6 +1231,37 @@ void renderMain() { ScrollWhenDraggingOnVoid(ImVec2(-mouse_delta.x, -mouse_delta.y), ImGuiMouseButton_Left); ImGui::EndChild(); } + + { + if (ImGui::Button("Broadcast", { 0.40f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { + g_buffer.inputUI.update = true; + g_buffer.inputUI.message = { + false, + std::chrono::system_clock::now(), + ::generateFileBroadcastMessage(), + settings.protocolId, + settings.volume, + Message::FileBroadcast + }; + + messageHistory.push_back(g_buffer.inputUI.message); + + g_tLastBroadcast = ImGui::GetTime(); + g_fileServer.startListening(); + } + ImGui::SameLine(); + + if (ImGui::ButtonDisablable("Stop", { 0.50f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight }, !g_fileServer.isListening())) { + g_fileServer.stopListening(); + } + ImGui::SameLine(); + + if (ImGui::ButtonDisablable("Clear", { 1.0f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight }, !hasAtLeastOneFile)) { + g_deleteInfo.uri = "###ALL-FILES###"; + g_deleteInfo.filename = ""; + g_deleteId++; + } + } } break; case SubWindowId::Receive: @@ -1183,7 +1295,7 @@ void renderMain() { if (fileInfoExtended.readyToShare) { ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Ready to share!"); } - } else if (fileInfoExtended.isReceiving || fileInfoExtended.nReceivedChunks > 0) { + } else if (g_fileClient.isConnected() && (fileInfoExtended.isReceiving || fileInfoExtended.nReceivedChunks > 0)) { if (fileInfoExtended.isReceiving) { if (ImGui::Button("Pause")) { fileInfoExtended.isReceiving = false; @@ -1196,12 +1308,14 @@ void renderMain() { ImGui::SameLine(); ImGui::ProgressBar(float(fileInfoExtended.nReceivedChunks)/fileInfo.second.nChunks); - } else { + } else if (g_fileClient.isConnected()) { if (ImGui::Button("Receive")) { fileInfoExtended.isReceiving = true; fileInfoExtended.isChunkReceived.resize(fileInfo.second.nChunks); fileInfoExtended.isChunkRequested.resize(fileInfo.second.nChunks); } + } else { + ImGui::Text("%s", ""); } if ((fileInfoExtended.isReceiving == false || isReceived) && fileInfoExtended.requestToShare == false) { @@ -1225,6 +1339,7 @@ void renderMain() { { const auto wSize = ImVec2 { ImGui::GetContentRegionAvailWidth(), ImGui::GetContentRegionAvail().y - subWindowButtonHeight - style.ItemInnerSpacing.y }; + ImGui::BeginChild("Files:Receive:status", wSize, true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); ImGui::PushTextWrapPos(); @@ -1243,25 +1358,27 @@ void renderMain() { ImGui::TextColored({ 1.0f, 1.0f, 0.0f, 1.0f }, "Attempting to connect ..."); } } + + ImGui::PopTextWrapPos(); + + ScrollWhenDraggingOnVoid(ImVec2(-mouse_delta.x, -mouse_delta.y), ImGuiMouseButton_Left); + ImGui::EndChild(); } - ImGui::PopTextWrapPos(); - - ScrollWhenDraggingOnVoid(ImVec2(-mouse_delta.x, -mouse_delta.y), ImGuiMouseButton_Left); - ImGui::EndChild(); - - if (g_hasRemoteInfo && g_fileClient.isConnecting() == false && g_fileClient.isConnected() == false) { - if (ImGui::Button("Connect", { 1.00f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { - g_fileClient.connect(g_remoteIP, g_remotePort, 0); + { + if (g_fileClient.isConnecting() == false && g_fileClient.isConnected() == false) { + if (ImGui::ButtonDisablable("Connect", { 1.00f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight }, !g_hasRemoteInfo)) { + g_fileClient.connect(g_remoteIP, g_remotePort, 0); + } } - } - if (g_fileClient.isConnecting() || g_fileClient.isConnected()) { - if (ImGui::Button("Disconnect", { 1.00f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { - g_fileClient.disconnect(); - g_hasReceivedFileInfos = false; - g_hasRequestedFileInfos = false; - g_hasReceivedFiles = false; + if (g_fileClient.isConnecting() || g_fileClient.isConnected()) { + if (ImGui::Button("Disconnect", { 1.00f*ImGui::GetContentRegionAvailWidth(), subWindowButtonHeight })) { + g_fileClient.disconnect(); + g_hasReceivedFileInfos = false; + g_hasRequestedFileInfos = false; + g_hasReceivedFiles = false; + } } } } diff --git a/examples/ggwave-gui/main.cpp b/examples/ggwave-gui/main.cpp index cccc32b..02f832d 100644 --- a/examples/ggwave-gui/main.cpp +++ b/examples/ggwave-gui/main.cpp @@ -43,7 +43,10 @@ bool ImGui_BeginFrame(SDL_Window * window) { printf("Dropped file: '%s'\n", event.drop.file); auto data = readFile(event.drop.file); std::string uri = event.drop.file; - auto filename = uri.substr(uri.find_last_of("/\\") + 1); + std::string filename = event.drop.file; + if (uri.find("/") || uri.find("\\")) { + filename = uri.substr(uri.find_last_of("/\\") + 1); + } addFile(uri.c_str(), filename.c_str(), std::move(data)); break; } @@ -175,9 +178,9 @@ int main(int argc, char** argv) { auto worker = initMain(); // tmp - addFile("test0.raw", "test0.raw", std::vector(1024)); - addFile("test1.jpg", "test0.jpg", std::vector(1024*1024 + 624)); - addFile("test2.mpv", "test0.mov", std::vector(1024*1024*234 + 53827)); + //addFile("test0.raw", "test0.raw", std::vector(1024)); + //addFile("test1.jpg", "test0.jpg", std::vector(1024*1024 + 624)); + //addFile("test2.mpv", "test0.mov", std::vector(1024*1024*234 + 53827)); while (true) { if (ImGui_BeginFrame(window) == false) { diff --git a/examples/third-party/ggsock b/examples/third-party/ggsock index 13852a1..75b9ee1 160000 --- a/examples/third-party/ggsock +++ b/examples/third-party/ggsock @@ -1 +1 @@ -Subproject commit 13852a1cf990a596227176a5aeace055b4eed12e +Subproject commit 75b9ee1bcf7991704296de13f31cb6054234f8e0