From ad1844adc72c771764f3d822d5a2a13f066b0cd5 Mon Sep 17 00:00:00 2001 From: Yousef Khadadeh Date: Thu, 14 May 2026 11:53:36 +0300 Subject: [PATCH] Improved android networking --- CMakeLists.txt | 1 + android-project/app/build.gradle | 2 +- android-project/app/proguard-rules.pro | 2 + .../app/src/main/AndroidManifest.xml | 10 + .../erroratline0/infinipaint/InfiniPaint.java | 7 + .../InfiniPaintNetworkService.java | 58 +++++ include/Helpers/Networking/NetLibrary.cpp | 25 +- include/Helpers/Networking/NetLibrary.hpp | 6 +- src/AndroidJNICalls.cpp | 35 ++- src/AndroidJNICalls.hpp | 2 + src/DrawingProgram/Tools/GridModifyTool.cpp | 16 +- src/DrawingProgram/Tools/GridModifyTool.hpp | 1 + src/MainProgram.cpp | 38 ++-- src/MainProgram.hpp | 2 + src/NetThreadManager.cpp | 79 +++++++ src/NetThreadManager.hpp | 27 +++ src/Screens/DesktopDrawingProgramScreen.cpp | 11 + src/Screens/DesktopDrawingProgramScreen.hpp | 1 + src/Screens/FileSelectScreen.cpp | 71 +++++- src/Screens/FileSelectScreen.hpp | 6 + src/Screens/PhoneDrawingProgramScreen.cpp | 214 +++++++++++++++--- src/Screens/PhoneDrawingProgramScreen.hpp | 7 +- src/Screens/Screen.cpp | 2 + src/Screens/Screen.hpp | 2 + src/Toolbar.cpp | 7 +- src/World.cpp | 1 + src/main.cpp | 4 + 27 files changed, 568 insertions(+), 69 deletions(-) create mode 100644 android-project/app/src/main/java/com/erroratline0/infinipaint/InfiniPaintNetworkService.java create mode 100644 src/NetThreadManager.cpp create mode 100644 src/NetThreadManager.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0eb8b14..a70e478 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,7 @@ set(sources "src/main.cpp" "src/GlobalConfig.cpp" "src/RichTextUserInput.cpp" "src/CustomEvents.cpp" + "src/NetThreadManager.cpp" "src/Screens/Screen.cpp" "src/Screens/DrawingProgramScreen.cpp" "src/Screens/DesktopDrawingProgramScreen.cpp" diff --git a/android-project/app/build.gradle b/android-project/app/build.gradle index 769a7a2..e9adba0 100644 --- a/android-project/app/build.gradle +++ b/android-project/app/build.gradle @@ -32,7 +32,7 @@ android { ndkVersion = "29.0.14206865" defaultConfig { applicationId 'com.erroratline0.InfiniPaint' - minSdkVersion 21 + minSdkVersion 26 targetSdk 35 versionCode 1 versionName "1.0" diff --git a/android-project/app/proguard-rules.pro b/android-project/app/proguard-rules.pro index 3156ca7..6fac289 100644 --- a/android-project/app/proguard-rules.pro +++ b/android-project/app/proguard-rules.pro @@ -20,6 +20,8 @@ void startTextInput(long, java.lang.String, int, int, int); void updateCursorPos(int, int); void updateTextboxAndCursorPos(java.lang.String, int, int); + void startNetworkService(); + void stopNetworkService(); } -keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLActivity { diff --git a/android-project/app/src/main/AndroidManifest.xml b/android-project/app/src/main/AndroidManifest.xml index b8111a8..7f52343 100644 --- a/android-project/app/src/main/AndroidManifest.xml +++ b/android-project/app/src/main/AndroidManifest.xml @@ -58,6 +58,9 @@ + + + + + + (level)) + "] " + message); }); @@ -104,7 +101,11 @@ void NetLibrary::init(const std::filesystem::path& p2pConfigPath) { } break; } +} +void NetLibrary::init_websocket() { + if(alreadyInitialized) + return; // Not sure why, but TLS verification fails on mac // Should be fixed later, but for now this is fine, as the signaling server doesn't have // critical information @@ -130,7 +131,7 @@ void NetLibrary::init(const std::filesystem::path& p2pConfigPath) { ws->onClosed([]() { Logger::get().log("INFO", "Websocket closed"); - destroy(); + destroy_websocket(); }); ws->onMessage([wws = make_weak_ptr(ws)](auto data) { @@ -187,7 +188,6 @@ void NetLibrary::init(const std::filesystem::path& p2pConfigPath) { Logger::get().log("INFO", "NetLibrary waiting for signaling to be connected..."); //wsFuture.get(); - alreadyInitialized = true; } @@ -228,7 +228,7 @@ void NetLibrary::register_server(std::shared_ptr server) { void NetLibrary::register_client(std::shared_ptr client) { client->localID = Random::get().alphanumeric_str(LOCALID_LEN); - std::scoped_lock serverListLock(clientListMutex); + std::scoped_lock clientListLock(clientListMutex); clients.emplace_back(client); } @@ -429,7 +429,14 @@ MessageOrder NetLibrary::get_message_order(rtc::binary& message) { return u.o; } -void NetLibrary::destroy() { +bool NetLibrary::is_initialized() { + return alreadyInitialized; +} + +void NetLibrary::destroy_websocket() { + if(!alreadyInitialized) + return; + ws = nullptr; { std::scoped_lock peerListLock(peerListMutex); @@ -454,6 +461,6 @@ void NetLibrary::destroy() { servers.clear(); } wsPromise = std::promise(); - //globalID.clear(); // Do we need to do this? (Causes problems when global ID changes from the user's side after they copy a lobby address alreadyInitialized = false; + //globalID.clear(); // Do we need to do this? (Causes problems when global ID changes from the user's side after they copy a lobby address } diff --git a/include/Helpers/Networking/NetLibrary.hpp b/include/Helpers/Networking/NetLibrary.hpp index 1dc56f6..2d4f586 100644 --- a/include/Helpers/Networking/NetLibrary.hpp +++ b/include/Helpers/Networking/NetLibrary.hpp @@ -39,7 +39,8 @@ typedef uint8_t NetTypeIDType; class NetLibrary { public: - static void init(const std::filesystem::path& p2pConfigPath); + static void init_config(const std::filesystem::path& p2pConfigPath); + static void init_websocket(); static void copy_default_p2p_config_to_path(const std::filesystem::path& newP2PConfigPath); static void update(); static std::string get_random_server_local_id(); @@ -48,7 +49,8 @@ class NetLibrary { static void register_server(std::shared_ptr server); static void register_client(std::shared_ptr client); - static void destroy(); + static bool is_initialized(); + static void destroy_websocket(); static constexpr size_t LOCALID_LEN = 10; static constexpr size_t GLOBALID_LEN = 40; diff --git a/src/AndroidJNICalls.cpp b/src/AndroidJNICalls.cpp index 175c6b2..f08bffe 100644 --- a/src/AndroidJNICalls.cpp +++ b/src/AndroidJNICalls.cpp @@ -25,6 +25,7 @@ #include #include #include +#include "NetThreadManager.hpp" std::string jstring2string(JNIEnv *env, jstring jStr) { if (!jStr) @@ -94,6 +95,26 @@ namespace AndroidJNICalls { env->DeleteLocalRef(clazz); } + void startNetworkService() { + JNIEnv* env = static_cast(SDL_GetAndroidJNIEnv()); + jobject activity = (jobject)SDL_GetAndroidActivity(); + jclass clazz = env->GetObjectClass(activity); + jmethodID method_id = env->GetStaticMethodID(clazz, "startNetworkService", "()V"); + env->CallStaticVoidMethod(clazz, method_id); + env->DeleteLocalRef(activity); + env->DeleteLocalRef(clazz); + } + + void stopNetworkService() { + JNIEnv* env = static_cast(SDL_GetAndroidJNIEnv()); + jobject activity = (jobject)SDL_GetAndroidActivity(); + jclass clazz = env->GetObjectClass(activity); + jmethodID method_id = env->GetStaticMethodID(clazz, "stopNetworkService", "()V"); + env->CallStaticVoidMethod(clazz, method_id); + env->DeleteLocalRef(activity); + env->DeleteLocalRef(clazz); + } + void updateModMap(CustomEvents::InputTextBoxID tId, const std::shared_ptr& newModMap) { std::scoped_lock a{textboxMutex}; if(tId == textboxID && newModMap && modMap) @@ -309,4 +330,16 @@ Java_com_erroratline0_infinipaint_InfiniPaintTextBoxInputConnection_nativeSetSel }); } -#endif +extern "C" +JNIEXPORT void JNICALL +Java_com_erroratline0_infinipaint_InfiniPaintNetworkService_networkServiceStarted(JNIEnv *env, jclass clazz) { + NetThreadManager::get().init_thread(); +} + +extern "C" +JNIEXPORT void JNICALL +Java_com_erroratline0_infinipaint_InfiniPaintNetworkService_networkServiceDestroyed(JNIEnv *env, jclass clazz) { + NetThreadManager::get().destroy_thread(); +} + +#endif \ No newline at end of file diff --git a/src/AndroidJNICalls.hpp b/src/AndroidJNICalls.hpp index 4cf6fab..e947866 100644 --- a/src/AndroidJNICalls.hpp +++ b/src/AndroidJNICalls.hpp @@ -27,6 +27,8 @@ namespace AndroidJNICalls { extern InputManager* globalInputManager; void startTextInput(CustomEvents::InputTextBoxID newTextboxID, const std::shared_ptr& newTextbox, const std::shared_ptr& newCursor, const std::shared_ptr& modMap, int inputType); + void startNetworkService(); + void stopNetworkService(); void updateCursorPos(CustomEvents::InputTextBoxID tId, const std::shared_ptr& newCursor); void updateModMap(CustomEvents::InputTextBoxID tId, const std::shared_ptr& newModMap); void updateTextboxAndCursorPos(CustomEvents::InputTextBoxID tId, const std::shared_ptr& newTextbox, const std::shared_ptr& newCursor); diff --git a/src/DrawingProgram/Tools/GridModifyTool.cpp b/src/DrawingProgram/Tools/GridModifyTool.cpp index 67f05f5..82b5119 100644 --- a/src/DrawingProgram/Tools/GridModifyTool.cpp +++ b/src/DrawingProgram/Tools/GridModifyTool.cpp @@ -23,7 +23,7 @@ #include "Helpers/NetworkingObjects/NetObjTemporaryPtr.decl.hpp" #include #include - +#include "../../Screens/PhoneDrawingProgramScreen.hpp" #include "../../GUIStuff/ElementHelpers/TextLabelHelpers.hpp" #include "../../GUIStuff/ElementHelpers/LayoutHelpers.hpp" #include "../../GUIStuff/ElementHelpers/TextBoxHelpers.hpp" @@ -130,6 +130,16 @@ void GridModifyTool::input_mouse_motion_callback(const InputManager::MouseMotion } } +Vector4f* GridModifyTool::color_picker_color(Vector4f* oldColor) { + NetworkingObjects::NetObjTemporaryPtr gLock = grid.lock(); + if(gLock) { + WorldGrid& g = *gLock; + if(oldColor == &g.color) + return oldColor; + } + return nullptr; +} + void GridModifyTool::gui_toolbox(Toolbar& t) { using namespace GUIStuff; using namespace ElementHelpers; @@ -209,6 +219,10 @@ void GridModifyTool::gui_phone_toolbox(PhoneDrawingProgramScreen& t) { checkbox_boolean_field(gui, "Subdivide outwards", "Subdivide when zooming out", &g.removeDivisionsOutwards, [&] { g.set_remove_divisions_outwards(g.removeDivisionsOutwards); }); + left_to_right_line_layout(gui, [&]() { + t.color_selector_button("Grid Color", &g.color); + text_label(gui, "Grid Color"); + }); auto bounded = std::make_shared(g.bounds.has_value()); checkbox_boolean_field(gui, "Bounded", "Bounded", bounded.get(), [&, bounded] { if(*bounded) { diff --git a/src/DrawingProgram/Tools/GridModifyTool.hpp b/src/DrawingProgram/Tools/GridModifyTool.hpp index c8e7332..4e4167f 100644 --- a/src/DrawingProgram/Tools/GridModifyTool.hpp +++ b/src/DrawingProgram/Tools/GridModifyTool.hpp @@ -28,6 +28,7 @@ class GridModifyTool : public DrawingProgramToolBase { GridModifyTool(DrawingProgram& initDrawP); void set_grid(const NetworkingObjects::NetObjWeakPtr& newGrid); virtual DrawingProgramToolType get_type() override; + virtual Vector4f* color_picker_color(Vector4f* oldColor) override; virtual void gui_phone_toolbox(PhoneDrawingProgramScreen& t) override; virtual void gui_toolbox(Toolbar& t) override; virtual void right_click_popup_gui(Toolbar& t, Vector2f popupPos) override; diff --git a/src/MainProgram.cpp b/src/MainProgram.cpp index f3890a4..e4dfde3 100755 --- a/src/MainProgram.cpp +++ b/src/MainProgram.cpp @@ -30,6 +30,7 @@ #include #include #include "InputManager.hpp" +#include "NetThreadManager.hpp" #include #include #include @@ -90,12 +91,20 @@ void MainProgram::update() { input.update(); g.update(); screen->update(); - for(auto& w : worlds) - w->update(); - NetLibrary::update(); + background_update(); post_callback(); } +void MainProgram::background_update() { + bool isAnyWorldConnected = false; + for(auto& w : worlds) { + w->update(); + isAnyWorldConnected |= ((w->netServer && !w->netServer->is_disconnected()) || (w->netClient && !w->netClient->is_disconnected())); + } + if(!isAnyWorldConnected) + NetThreadManager::get().destroy(); +} + bool MainProgram::app_close_requested() { return screen->app_close_requested(); } @@ -108,7 +117,7 @@ void MainProgram::update_display_names() { } void MainProgram::init_net_library() { - NetLibrary::init(conf.configPath / "p2p.json"); + NetThreadManager::get().init(this); } void MainProgram::save_config() { @@ -183,6 +192,7 @@ void MainProgram::load_config() { conf.load_licenses(); NetLibrary::copy_default_p2p_config_to_path(conf.configPath / "p2p.json"); + NetLibrary::init_config(conf.configPath / "p2p.json"); update_display_names(); set_vsync_value(conf.vsyncValue); @@ -284,8 +294,8 @@ void MainProgram::switch_to_tab(size_t wIndex) { void MainProgram::post_callback() { g.gui.run_post_callback_func(); - run_new_screen_func(); close_set_to_close_tabs(); + run_new_screen_func(); g.gui.layout_if_necessary(); } @@ -293,6 +303,13 @@ void MainProgram::input_app_about_to_go_to_background_callback() { screen->input_app_about_to_go_to_background_callback(); save_config(); post_callback(); + NetThreadManager::get().go_to_background(); +} + +void MainProgram::input_app_about_to_go_to_foreground_callback() { + NetThreadManager::get().go_to_foreground(); + screen->input_app_about_to_go_to_foreground_callback(); + post_callback(); } void MainProgram::input_add_file_to_canvas_callback(const CustomEvents::AddFileToCanvasEvent& addFile) { @@ -488,15 +505,8 @@ void MainProgram::close_set_to_close_tabs() { } return false; }); - if(worlds.empty()) - create_new_tab({ - .isClient = false - }); - else if(world) - worldIndex = std::find(worlds.begin(), worlds.end(), world) - worlds.begin(); - else - switch_to_tab(0); tabsToClose.clear(); + screen->on_tab_close(); g.gui.set_to_layout(); } } @@ -524,5 +534,5 @@ void MainProgram::early_destroy() { MainProgram::~MainProgram() { screen = nullptr; // Destroy screen first to let destructor run - NetLibrary::destroy(); + NetThreadManager::get().destroy(); } diff --git a/src/MainProgram.hpp b/src/MainProgram.hpp index f50d8f2..7f9cf77 100644 --- a/src/MainProgram.hpp +++ b/src/MainProgram.hpp @@ -111,6 +111,7 @@ class MainProgram { MainProgram(); void update(); + void background_update(); void draw_world(SkCanvas* canvas, std::shared_ptr worldToDraw, const DrawData& drawData); void draw(SkCanvas* canvas); sk_sp create_native_surface(Vector2i resolution, bool isMSAA); @@ -156,6 +157,7 @@ class MainProgram { void input_android_text_box_input_callback(const CustomEvents::AndroidTextBoxInputEvent& textboxInput); void input_app_about_to_go_to_background_callback(); + void input_app_about_to_go_to_foreground_callback(); void input_global_back_button_callback(); bool input_keybind_callback(const Vector2ui32& newKey); void input_drop_file_callback(const InputManager::DropCallbackArgs& drop); diff --git a/src/NetThreadManager.cpp b/src/NetThreadManager.cpp new file mode 100644 index 0000000..8575e54 --- /dev/null +++ b/src/NetThreadManager.cpp @@ -0,0 +1,79 @@ +#include "NetThreadManager.hpp" +#include +#include "AndroidJNICalls.hpp" +#include "MainProgram.hpp" + +NetThreadManager NetThreadManager::global; + +NetThreadManager& NetThreadManager::get() { + return global; +} + +void NetThreadManager::init(MainProgram* m) { + if(started) + return; + started = true; + main = m; +#ifndef __ANDROID__ + init_thread(); +#else + AndroidJNICalls::startNetworkService(); +#endif +} + +void NetThreadManager::init_thread() { + if(t) + throw std::runtime_error("[NetThreadManager::init_thread] Thread can't exist when init_thread is called"); + destroyThread = false; + t = std::make_unique([&]{ thread_update(); }); +} + +void NetThreadManager::thread_update() { + NetLibrary::init_websocket(); + for(;;) { + using namespace std::chrono_literals; + std::this_thread::sleep_for(20ms); + if(destroyThread) { + NetLibrary::destroy_websocket(); + break; + } + { + std::scoped_lock l{backgroundUpdateMutex}; + if(backgroundUpdate) + main->background_update(); + } + NetLibrary::update(); + } +#ifdef __ANDROID__ + AndroidJNICalls::stopNetworkService(); +#endif +} + +void NetThreadManager::destroy_thread() { + if(!t) + throw std::runtime_error("[NetThreadManager::init_thread] Thread can't be null when destroy_thread is called"); + destroyThread = true; + t->join(); + t = nullptr; +} + +void NetThreadManager::go_to_background() { + std::scoped_lock l{backgroundUpdateMutex}; + backgroundUpdate = true; +} + +void NetThreadManager::go_to_foreground() { + std::scoped_lock l{backgroundUpdateMutex}; + backgroundUpdate = false; +} + +void NetThreadManager::destroy() { + if(!started) + return; + started = false; +#ifndef __ANDROID__ + destroy_thread(); +#else + AndroidJNICalls::stopNetworkService(); +#endif +} diff --git a/src/NetThreadManager.hpp b/src/NetThreadManager.hpp new file mode 100644 index 0000000..fa5edd9 --- /dev/null +++ b/src/NetThreadManager.hpp @@ -0,0 +1,27 @@ +#pragma once +#include +#include +#include + +class MainProgram; + +class NetThreadManager { + public: + static NetThreadManager& get(); + void init(MainProgram* m); + void thread_update(); + void destroy(); + void init_thread(); + void destroy_thread(); + void go_to_background(); + void go_to_foreground(); + private: + static NetThreadManager global; + MainProgram* main; + std::mutex backgroundUpdateMutex; + bool backgroundUpdate = false; + std::unique_ptr t; + std::atomic destroyThread = false; + + bool started = false; +}; diff --git a/src/Screens/DesktopDrawingProgramScreen.cpp b/src/Screens/DesktopDrawingProgramScreen.cpp index 0547677..4bc4120 100644 --- a/src/Screens/DesktopDrawingProgramScreen.cpp +++ b/src/Screens/DesktopDrawingProgramScreen.cpp @@ -83,3 +83,14 @@ void DesktopDrawingProgramScreen::open_file_selector(const std::string& filePick else toolbar.open_file_selector_non_native(filePickerName, extensionFilters, postSelectionFunc, fileName, isSaving); } + +void DesktopDrawingProgramScreen::on_tab_close() { + if(main.worlds.empty()) + main.create_new_tab({ + .isClient = false + }); + else if(main.world) + main.worldIndex = std::find(main.worlds.begin(), main.worlds.end(), main.world) - main.worlds.begin(); + else + main.switch_to_tab(0); +} diff --git a/src/Screens/DesktopDrawingProgramScreen.hpp b/src/Screens/DesktopDrawingProgramScreen.hpp index d2ffc49..0658f82 100644 --- a/src/Screens/DesktopDrawingProgramScreen.hpp +++ b/src/Screens/DesktopDrawingProgramScreen.hpp @@ -29,6 +29,7 @@ class DesktopDrawingProgramScreen : public DrawingProgramScreen { virtual void input_key_callback(const InputManager::KeyCallbackArgs& key) override; virtual void input_text_key_callback(const InputManager::KeyCallbackArgs& key) override; virtual void open_file_selector(const std::string& filePickerName, const std::vector& extensionFilters, OpenFileSelectorCallback postSelectionFunc, const std::string& fileName = "", bool isSaving = false) override; + virtual void on_tab_close() override; private: Toolbar toolbar; diff --git a/src/Screens/FileSelectScreen.cpp b/src/Screens/FileSelectScreen.cpp index be107f1..58e9848 100644 --- a/src/Screens/FileSelectScreen.cpp +++ b/src/Screens/FileSelectScreen.cpp @@ -220,6 +220,9 @@ void FileSelectScreen::main_display() { case SelectedMenu::TRASH: file_view(); break; + case SelectedMenu::CONNECT: + connect_view(); + break; case SelectedMenu::SETTINGS: settings_view(); break; @@ -491,13 +494,16 @@ void FileSelectScreen::title_bar() { }); switch(selectedMenu) { case SelectedMenu::FILES: - mutable_text_label(gui, "main screen header text", "Files"); + mutable_text_label(gui, "main screen header text files", "Files"); break; case SelectedMenu::TRASH: - mutable_text_label(gui, "main screen header text", "Trash"); + mutable_text_label(gui, "main screen header text trash", "Trash"); + break; + case SelectedMenu::CONNECT: + mutable_text_label(gui, "main screen header text connect", "Connect"); break; case SelectedMenu::SETTINGS: - mutable_text_label(gui, "main screen header text", "Settings"); + mutable_text_label(gui, "main screen header text settings", "Settings"); break; } CLAY_AUTO_ID({ @@ -696,6 +702,11 @@ void FileSelectScreen::main_menu() { mainMenuOpenAnim->animation_trigger_reverse(); if(mainViewScrollArea) mainViewScrollArea->reset_scroll(); }); + icon_text_transparent_option_selected_button("Connect", "data/icons/network.svg", "Connect", selectedMenu == SelectedMenu::CONNECT, [&] { + selectedMenu = SelectedMenu::CONNECT; + mainMenuOpenAnim->animation_trigger_reverse(); + if(mainViewScrollArea) mainViewScrollArea->reset_scroll(); + }); icon_text_transparent_option_selected_button("Settings", "data/icons/RemixIcon/settings-3-line.svg", "Settings", selectedMenu == SelectedMenu::SETTINGS, [&] { selectedMenu = SelectedMenu::SETTINGS; mainMenuOpenAnim->animation_trigger_reverse(); @@ -854,6 +865,50 @@ void FileSelectScreen::file_view() { } } +void FileSelectScreen::connect_view() { + auto& gui = main.g.gui; + + mainViewScrollArea = gui.element("connect scroll area", ScrollArea::Options{ + .scrollVertical = true, + .clipVertical = true, + .scrollbarY = ScrollArea::ScrollbarType::NORMAL, + .innerContent = [&] (auto&) { + CLAY_AUTO_ID({ + .layout = { + .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}, + .layoutDirection = CLAY_LEFT_TO_RIGHT + }, + }) { + CLAY_AUTO_ID({ + .layout = { + .sizing = {.width = CLAY_SIZING_GROW(0, 300), .height = CLAY_SIZING_GROW(0)}, + .childGap = gui.io.theme->childGap1, + .layoutDirection = CLAY_TOP_TO_BOTTOM + }, + }) { + input_text_field(gui, "Connect lobby field", "Lobby", &connectLobbyStr); + text_button(gui, "Connect button", "Connect", { + .wide = true, + .onClick = [&] { + CustomEvents::emit_event({ + .isClient = true, + .netSource = connectLobbyStr + }); + } + }); + text_button(gui, "Paste and Connect button", "Paste & Connect", { + .wide = true, + .onClick = [&] { + connectOnPaste = true; + main.input.call_paste(CustomEvents::PasteEvent::DataType::TEXT, { .allowRichText = false }); + } + }); + } + } + } + }); +} + void FileSelectScreen::settings_view() { auto& gui = main.g.gui; @@ -875,6 +930,7 @@ void FileSelectScreen::settings_view() { .layoutDirection = CLAY_TOP_TO_BOTTOM }, }) { + input_text_field(gui, "display name input", "Display name", &main.conf.displayName); color_picker_button_field(gui, "defaultCanvasBackgroundColor", "Default canvas background color", &main.conf.defaultCanvasBackgroundColor, { .hasAlpha = false }); input_scalar_field(gui, "Max GUI Scale", "Max GUI Scale", &main.conf.guiScale, 1.0f, 2.0f, { .decimalPrecision = 1, @@ -926,6 +982,15 @@ void FileSelectScreen::start_edit_mode() { editMode = true; } +void FileSelectScreen::input_paste_callback(const CustomEvents::PasteEvent& paste) { + if(connectOnPaste && paste.type == CustomEvents::PasteEvent::DataType::TEXT) { + CustomEvents::emit_event({ + .isClient = true, + .netSource = paste.data + }); + } +} + void FileSelectScreen::input_open_infinipaint_file_callback(const CustomEvents::OpenInfiniPaintFileEvent& openFile) { main.create_new_tab(openFile); main.set_screen([&] (std::unique_ptr) { return std::make_unique(main); }); diff --git a/src/Screens/FileSelectScreen.hpp b/src/Screens/FileSelectScreen.hpp index 2f0c4c9..634bf05 100644 --- a/src/Screens/FileSelectScreen.hpp +++ b/src/Screens/FileSelectScreen.hpp @@ -27,6 +27,7 @@ class FileSelectScreen : public Screen { FileSelectScreen(MainProgram& m); virtual void gui_layout_run() override; virtual void draw(SkCanvas* canvas) override; + virtual void input_paste_callback(const CustomEvents::PasteEvent& paste) override; virtual void input_open_infinipaint_file_callback(const CustomEvents::OpenInfiniPaintFileEvent& openFile) override; virtual void input_global_back_button_callback() override; virtual void input_app_about_to_go_to_background_callback() override; @@ -65,6 +66,9 @@ class FileSelectScreen : public Screen { void save_files(); + bool connectOnPaste = false; + std::string connectLobbyStr; + struct FileInfo { std::string fileName; SDL_Time lastModifyTime; @@ -85,6 +89,7 @@ class FileSelectScreen : public Screen { void main_display(); void main_menu(); void file_view(); + void connect_view(); void settings_view(); void create_file_button(); void file_view_edit(); @@ -115,6 +120,7 @@ class FileSelectScreen : public Screen { enum class SelectedMenu { FILES, TRASH, + CONNECT, SETTINGS } selectedMenu = SelectedMenu::FILES; diff --git a/src/Screens/PhoneDrawingProgramScreen.cpp b/src/Screens/PhoneDrawingProgramScreen.cpp index 1285750..353367e 100644 --- a/src/Screens/PhoneDrawingProgramScreen.cpp +++ b/src/Screens/PhoneDrawingProgramScreen.cpp @@ -29,6 +29,7 @@ #include "../GUIStuff/ElementHelpers/TextLabelHelpers.hpp" #include "../GUIStuff/ElementHelpers/TextBoxHelpers.hpp" #include "FileSelectScreen.hpp" +#include "../NetThreadManager.hpp" #include using namespace GUIStuff; @@ -71,9 +72,21 @@ void PhoneDrawingProgramScreen::center_message(const char* id, const std::string } void PhoneDrawingProgramScreen::gui_layout_run() { + gui_layout_safety_checks(); main_display(); } +void PhoneDrawingProgramScreen::gui_layout_safety_checks() { + if(!main.world->netServer) { + if(topToolbarSettingsPopup == TopToolbarSettingsPopup::LOBBY_INFO) + topToolbarSettingsPopup = TopToolbarSettingsPopup::NONE; + } + if(!main.world->netClient && !main.world->netServer) { + if(topToolbarSettingsPopup == TopToolbarSettingsPopup::PLAYER_LIST) + topToolbarSettingsPopup = TopToolbarSettingsPopup::NONE; + } +} + void PhoneDrawingProgramScreen::main_display() { CLAY_AUTO_ID({ .layout = { @@ -83,18 +96,22 @@ void PhoneDrawingProgramScreen::main_display() { .layoutDirection = CLAY_TOP_TO_BOTTOM }, }) { - if(hideGUI) - hidden_gui(); + if(main.world->netClient && main.world->clientStillConnecting) + center_message("Connecting to server message", "Connecting to server..."); else { - top_toolbar(); - top_toolbar_settings_popup(); - CLAY_AUTO_ID({ - .layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}} - }) {} - bottom_toolbar(); + if(hideGUI) + hidden_gui(); + else { + top_toolbar(); + top_toolbar_settings_popup(); + CLAY_AUTO_ID({ + .layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}} + }) {} + bottom_toolbar(); + } + if(!main.world->drawProg.layerMan.is_a_layer_being_edited()) + center_message("Select layer to edit message", "Select a layer to edit"); } - if(!main.world->drawProg.layerMan.is_a_layer_being_edited()) - center_message("Select layer to edit message", "Select a layer to edit"); } } @@ -241,6 +258,97 @@ void PhoneDrawingProgramScreen::top_toolbar_settings_popup() { }); break; } + case TopToolbarSettingsPopup::LOBBY_INFO: { + gui.set_z_index(gui.get_z_index() + 20, [&] { + gui.element("host lobby info menu", [&] (LayoutElement*, const Clay_ElementId& lId) { + Vector2f popupSize = { + std::clamp(gui.io.safeWindowRect.width() - 10.0f, 20.0f, 300.0f), + std::clamp(gui.io.safeWindowRect.height() - 40.0f, 50.0f, 600.0f) + }; + CLAY(lId, { + .layout = { + .sizing = {.width = CLAY_SIZING_FIT(popupSize.x()), .height = CLAY_SIZING_FIT(0, popupSize.y()) }, + .padding = CLAY_PADDING_ALL(io.theme->padding1), + .childGap = io.theme->childGap1, + .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP}, + .layoutDirection = CLAY_TOP_TO_BOTTOM + }, + .backgroundColor = convert_vec4(io.theme->backColor1), + .cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1), + .floating = {.offset = {.x = 0, .y = static_cast(io.theme->padding1)}, .zIndex = gui.get_z_index(), .attachPoints = {.element = CLAY_ATTACH_POINT_RIGHT_TOP, .parent = CLAY_ATTACH_POINT_RIGHT_BOTTOM}, .attachTo = CLAY_ATTACH_TO_PARENT} + }) { + text_label_centered(gui, "Lobby Info"); + input_text_field(gui, "lobby", "Lobby", &serverToConnectTo, { + .immutable = true + }); + text_button(gui, "Copy button", "Copy", { + .wide = true, + .onClick = [&] { + main.input.set_clipboard_str(serverToConnectTo); + topToolbarSettingsPopup = TopToolbarSettingsPopup::NONE; + } + }); + } + }); + }); + break; + } + case TopToolbarSettingsPopup::PLAYER_LIST: { + gui.set_z_index(gui.get_z_index() + 20, [&] { + gui.element("player list menu", [&] (LayoutElement*, const Clay_ElementId& lId) { + Vector2f popupSize = { + std::clamp(gui.io.safeWindowRect.width() - 10.0f, 20.0f, 300.0f), + std::clamp(gui.io.safeWindowRect.height() - 40.0f, 50.0f, 600.0f) + }; + CLAY(lId, { + .layout = { + .sizing = {.width = CLAY_SIZING_FIT(popupSize.x()), .height = CLAY_SIZING_FIT(0, popupSize.y()) }, + .padding = CLAY_PADDING_ALL(io.theme->padding1), + .childGap = io.theme->childGap1, + .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP}, + .layoutDirection = CLAY_TOP_TO_BOTTOM + }, + .backgroundColor = convert_vec4(io.theme->backColor1), + .cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1), + .floating = {.offset = {.x = 0, .y = static_cast(io.theme->padding1)}, .zIndex = gui.get_z_index(), .attachPoints = {.element = CLAY_ATTACH_POINT_RIGHT_TOP, .parent = CLAY_ATTACH_POINT_RIGHT_BOTTOM}, .attachTo = CLAY_ATTACH_TO_PARENT} + }) { + text_label_centered(gui, "Player List"); + left_to_right_line_layout(gui, [&]() { + CLAY_AUTO_ID({ + .layout = { + .sizing = {.width = CLAY_SIZING_FIXED(20), .height = CLAY_SIZING_FIXED(20)} + }, + .backgroundColor = convert_vec4(SkColor4f{main.world->ownClientData->get_cursor_color().x(), main.world->ownClientData->get_cursor_color().y(), main.world->ownClientData->get_cursor_color().z(), 1.0f}), + .cornerRadius = CLAY_CORNER_RADIUS(3) + }) {} + ellipse_wide_paragraph_label(gui, "own player name", main.world->ownClientData->get_display_name()); + }); + size_t num = 0; + for(auto& client : main.world->clients->get_data()) { + if(client != main.world->ownClientData) { + gui.new_id(num++, [&] { + left_to_right_line_layout(gui, [&]() { + CLAY_AUTO_ID({ + .layout = { + .sizing = {.width = CLAY_SIZING_FIXED(20), .height = CLAY_SIZING_FIXED(20)} + }, + .backgroundColor = convert_vec4(SkColor4f{client->get_cursor_color().x(), client->get_cursor_color().y(), client->get_cursor_color().z(), 1.0f}), + .cornerRadius = CLAY_CORNER_RADIUS(3) + }) {} + ellipse_wide_paragraph_label(gui, "other player name", client->get_display_name()); + text_button(gui, "teleport button", "Jump To", { .onClick = [&] { + main.world->drawData.cam.smooth_move_to(*main.world, client->get_cam_coords(), client->get_window_size()); + topToolbarSettingsPopup = TopToolbarSettingsPopup::NONE; + }}); + }); + }); + } + } + } + }); + }); + break; + } } } @@ -268,9 +376,8 @@ void PhoneDrawingProgramScreen::top_toolbar() { svg_icon_button(gui, "back exit button", "data/icons/RemixIcon/arrow-left-s-line.svg", { .drawType = SelectableButton::DrawType::TRANSPARENT_ALL, .onClick = [&] { - main.world->save_to_file(main.world->filePath); + save_to_file(); main.set_tab_to_close(main.world.get()); - main.set_screen([&] (std::unique_ptr) { return std::make_unique(main); }); } }); top_toolbar_remaining_area(); @@ -350,17 +457,24 @@ void PhoneDrawingProgramScreen::top_toolbar_remaining_area() { break; spaceForWorldName -= BUTTON_WIDTH; } - input_text(gui, "world name text", &main.world->name, { - .decorations = false, - .onSelect = [&] { - fileOldPath = main.world->filePath; - }, - .onDeselect = [&] { - std::filesystem::remove(fileOldPath); - std::filesystem::remove(fileOldPath.parent_path() / (fileOldPath.stem().string() + ".jpg")); - main.world->autosave_to_directory(main.world->filePath.parent_path()); - } - }); + if(main.world->netClient) { + CLAY_AUTO_ID({ + .layout = {.sizing = {.width = CLAY_SIZING_GROW(0)}} + }) { } + } + else { + input_text(gui, "world name text", &main.world->name, { + .decorations = false, + .onSelect = [&] { + fileOldPath = main.world->filePath; + }, + .onDeselect = [&] { + std::filesystem::remove(fileOldPath); + std::filesystem::remove(fileOldPath.parent_path() / (fileOldPath.stem().string() + ".jpg")); + main.world->autosave_to_directory(main.world->filePath.parent_path()); + } + }); + } gui.new_id("visible buttons", [&] { for(size_t i = 0; i < buttonFreeSpace; i++) { gui.new_id(i, [&] { @@ -418,15 +532,37 @@ void PhoneDrawingProgramScreen::top_toolbar_hidden_button_popup(GUIStuff::Elemen } }); - l.emplace_back(TopToolbarRemainingAreaButton{ - .name = "Host", - .svgPath = "data/icons/network.svg", - .onClick = [&] { - serverLocalID = NetLibrary::get_random_server_local_id(); - serverToConnectTo = NetLibrary::get_global_id() + serverLocalID; - topToolbarSettingsPopup = topToolbarSettingsPopup == TopToolbarSettingsPopup::HOST ? TopToolbarSettingsPopup::NONE : TopToolbarSettingsPopup::HOST; - } - }); + if(!main.world->netClient && !main.world->netServer) { + l.emplace_back(TopToolbarRemainingAreaButton{ + .name = "Host", + .svgPath = "data/icons/network.svg", + .onClick = [&] { + serverLocalID = NetLibrary::get_random_server_local_id(); + serverToConnectTo = NetLibrary::get_global_id() + serverLocalID; + topToolbarSettingsPopup = topToolbarSettingsPopup == TopToolbarSettingsPopup::HOST ? TopToolbarSettingsPopup::NONE : TopToolbarSettingsPopup::HOST; + } + }); + } + + if(main.world->netServer) { + l.emplace_back(TopToolbarRemainingAreaButton{ + .name = "Lobby Info", + .svgPath = "data/icons/network.svg", + .onClick = [&] { + topToolbarSettingsPopup = topToolbarSettingsPopup == TopToolbarSettingsPopup::LOBBY_INFO ? TopToolbarSettingsPopup::NONE : TopToolbarSettingsPopup::LOBBY_INFO; + } + }); + } + + if(main.world->netClient || main.world->netServer) { + l.emplace_back(TopToolbarRemainingAreaButton{ + .name = "Player List", + .svgPath = "data/icons/list.svg", + .onClick = [&] { + topToolbarSettingsPopup = topToolbarSettingsPopup == TopToolbarSettingsPopup::PLAYER_LIST ? TopToolbarSettingsPopup::NONE : TopToolbarSettingsPopup::PLAYER_LIST; + } + }); + } gui.set_z_index(gui.get_z_index() + 20, [&] { dropdown_many_element_popup_layout(gui, "top toolbar hidden button popup", { @@ -1017,15 +1153,23 @@ void PhoneDrawingProgramScreen::input_global_back_button_callback() { main.g.gui.set_to_layout(); } else { - main.world->save_to_file(main.world->filePath); + save_to_file(); main.set_tab_to_close(main.world.get()); main.g.gui.set_to_layout(); - main.set_screen([&] (std::unique_ptr) { return std::make_unique(main); }); } } +void PhoneDrawingProgramScreen::save_to_file() { + if(!main.world->netClient) + main.world->save_to_file(main.world->filePath); +} + void PhoneDrawingProgramScreen::input_app_about_to_go_to_background_callback() { - main.world->save_to_file(main.world->filePath); + save_to_file(); +} + +void PhoneDrawingProgramScreen::on_tab_close() { + main.set_screen([&] (std::unique_ptr) { return std::make_unique(main); }); } void PhoneDrawingProgramScreen::color_selector(Vector4f* color, const ColorSelectorData& colorSelectorData) { diff --git a/src/Screens/PhoneDrawingProgramScreen.hpp b/src/Screens/PhoneDrawingProgramScreen.hpp index 5d2e908..5385e66 100644 --- a/src/Screens/PhoneDrawingProgramScreen.hpp +++ b/src/Screens/PhoneDrawingProgramScreen.hpp @@ -28,6 +28,7 @@ class PhoneDrawingProgramScreen : public DrawingProgramScreen { virtual void gui_layout_run() override; virtual void input_global_back_button_callback() override; virtual void input_app_about_to_go_to_background_callback() override; + virtual void on_tab_close() override; struct ColorSelectorData { std::function onChange; std::function onSelect; @@ -73,7 +74,9 @@ class PhoneDrawingProgramScreen : public DrawingProgramScreen { BOOKMARKS, LAYERS, GRIDS, - HOST + HOST, + LOBBY_INFO, + PLAYER_LIST } topToolbarSettingsPopup = TopToolbarSettingsPopup::NONE; bool hideGUI = false; @@ -82,6 +85,7 @@ class PhoneDrawingProgramScreen : public DrawingProgramScreen { Vector4f* colorPickerPtr = nullptr; ColorSelectorData colorPickerData; + void gui_layout_safety_checks(); void hidden_gui(); void center_message(const char* id, const std::string& m); void default_bottom_toolbar(); @@ -95,6 +99,7 @@ class PhoneDrawingProgramScreen : public DrawingProgramScreen { void bottom_toolbar(); void main_display(); void tool_settings_popup(); + void save_to_file(); std::string serverToConnectTo; std::string serverLocalID; diff --git a/src/Screens/Screen.cpp b/src/Screens/Screen.cpp index f81a540..97cb3ee 100644 --- a/src/Screens/Screen.cpp +++ b/src/Screens/Screen.cpp @@ -83,6 +83,8 @@ void Screen::input_finger_motion_callback(const InputManager::FingerMotionCallba void Screen::input_window_resize_callback(const InputManager::WindowResizeCallbackArgs& w) {} void Screen::input_window_scale_callback(const InputManager::WindowScaleCallbackArgs& w) {} void Screen::input_app_about_to_go_to_background_callback() {} +void Screen::input_app_about_to_go_to_foreground_callback() {} +void Screen::on_tab_close() {} std::optional Screen::get_text_box_start_info() { return std::nullopt; } Screen::~Screen() {} diff --git a/src/Screens/Screen.hpp b/src/Screens/Screen.hpp index 156a234..e391053 100644 --- a/src/Screens/Screen.hpp +++ b/src/Screens/Screen.hpp @@ -65,6 +65,8 @@ class Screen { virtual void input_window_resize_callback(const InputManager::WindowResizeCallbackArgs& w); virtual void input_window_scale_callback(const InputManager::WindowScaleCallbackArgs& w); virtual void input_app_about_to_go_to_background_callback(); + virtual void input_app_about_to_go_to_foreground_callback(); + virtual void on_tab_close(); virtual std::optional get_text_box_start_info(); virtual void open_file_selector(const std::string& filePickerName, const std::vector& extensionFilters, OpenFileSelectorCallback postSelectionFunc, const std::string& fileName = "", bool isSaving = false); diff --git a/src/Toolbar.cpp b/src/Toolbar.cpp index acf4807..7225c3d 100644 --- a/src/Toolbar.cpp +++ b/src/Toolbar.cpp @@ -1601,8 +1601,11 @@ void Toolbar::general_settings_inner_gui() { switch(generalSettingsOptions) { case GSETTINGS_GENERAL: { general_scroll_area("general settings", [&] { - input_text_field(gui, "display name input", "Display name", &main.conf.displayName); - main.update_display_names(); + input_text_field(gui, "display name input", "Display name", &main.conf.displayName, { + .onEdit = [&] { + main.update_display_names(); + } + }); color_picker_button_field(gui, "defaultCanvasBackgroundColor", "Default canvas background color", &main.conf.defaultCanvasBackgroundColor, { .hasAlpha = false }); #ifndef __EMSCRIPTEN__ checkbox_boolean_field(gui, "native file pick", "Use native file picker", &main.conf.useNativeFilePicker); diff --git a/src/World.cpp b/src/World.cpp index 08fa560..344fe7e 100644 --- a/src/World.cpp +++ b/src/World.cpp @@ -227,6 +227,7 @@ bool World::connection_update() { Logger::get().log("USERINFO", "Client connection failed"); netObjMan.disconnect(); main.set_tab_to_close(this); + netClient = nullptr; return false; } netClient->update(); diff --git a/src/main.cpp b/src/main.cpp index 2fe65f6..a429271 100755 --- a/src/main.cpp +++ b/src/main.cpp @@ -670,6 +670,10 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { Logger::get().log("INFO", "[SDL_AppEvent] Entering background"); mS.m->input_app_about_to_go_to_background_callback(); break; + case SDL_EVENT_WILL_ENTER_FOREGROUND: + Logger::get().log("INFO", "[SDL_AppEvent] Entering foreground"); + mS.m->input_app_about_to_go_to_foreground_callback(); + break; case SDL_EVENT_WINDOW_CLOSE_REQUESTED: if(event->window.windowID == SDL_GetWindowID(mS.window)) { if(mS.m->app_close_requested())