diff --git a/include/Helpers/StringHelpers.cpp b/include/Helpers/StringHelpers.cpp index c3baf43..cfe7a9b 100644 --- a/include/Helpers/StringHelpers.cpp +++ b/include/Helpers/StringHelpers.cpp @@ -82,3 +82,43 @@ std::string remove_carriage_returns_from_str(std::string s) { std::erase(s, '\r'); return s; } + +std::string ensure_string_unique(const std::vector& stringList, std::string str) { + for(;;) { + bool isUnique = true; + for(const std::string& strToCheckAgainst : stringList) { + if(strToCheckAgainst == str) { + size_t leftParenthesisIndex = str.find_last_of('('); + size_t rightParenthesisIndex = str.find_last_of(')'); + bool failToIncrement = true; + if(leftParenthesisIndex != std::string::npos && rightParenthesisIndex != std::string::npos && leftParenthesisIndex < rightParenthesisIndex) { + std::string numStr = str.substr(leftParenthesisIndex + 1, rightParenthesisIndex - leftParenthesisIndex - 1); + bool isAllDigits = true; + for(char c : numStr) { + if(!isdigit(c)) { + isAllDigits = false; + break; + } + } + if(isAllDigits) { + try { + int s = std::stoi(numStr); + s++; + str = str.substr(0, leftParenthesisIndex); + str += "(" + std::to_string(s) + ")"; + failToIncrement = false; + } + catch(...) {} + } + } + if(failToIncrement) + str += " (2)"; + isUnique = false; + break; + } + } + if(isUnique) + break; + } + return str; +} diff --git a/include/Helpers/StringHelpers.hpp b/include/Helpers/StringHelpers.hpp index 006067a..709b33d 100644 --- a/include/Helpers/StringHelpers.hpp +++ b/include/Helpers/StringHelpers.hpp @@ -12,3 +12,4 @@ std::string byte_vector_to_hex_str(const std::vector& byteVec); std::string read_file_to_string(const std::filesystem::path& filePath); bool is_valid_http_url(const std::string& str); std::string remove_carriage_returns_from_str(std::string s); +std::string ensure_string_unique(const std::vector& stringList, std::string str); diff --git a/src/MainProgram.cpp b/src/MainProgram.cpp index 84ebba0..6f39bfe 100755 --- a/src/MainProgram.cpp +++ b/src/MainProgram.cpp @@ -80,10 +80,8 @@ void MainProgram::update() { return false; }); - if(setTabToClose) { - worlds.erase(worlds.begin() + setTabToClose.value()); - setTabToClose.reset(); - } + for(auto& s : setTabsToClose) + std::erase(worlds, s.lock()); std::erase_if(worlds, [&](const std::shared_ptr& w) { return w->setToDestroy; @@ -129,6 +127,10 @@ void MainProgram::update() { return; } +bool MainProgram::app_close_requested() { + return toolbar.app_close_requested(); +} + void MainProgram::update_display_names() { for(auto& w : worlds) { if(!w->netObjMan.is_connected()) @@ -157,8 +159,8 @@ void MainProgram::new_tab_open() { worldIndex = worlds.size() - 1; } -void MainProgram::set_tab_to_close(size_t tabToClose) { - setTabToClose = tabToClose; +void MainProgram::set_tab_to_close(const std::weak_ptr& tabToClose) { + setTabsToClose.emplace_back(tabToClose); } void MainProgram::init_net_library() { diff --git a/src/MainProgram.hpp b/src/MainProgram.hpp index 6b3e846..c7d5363 100644 --- a/src/MainProgram.hpp +++ b/src/MainProgram.hpp @@ -88,7 +88,7 @@ class MainProgram { void early_destroy(); void new_tab(const World::OpenWorldInfo& tabInfo, bool createSameThread = false); - void set_tab_to_close(size_t tabToClose); + void set_tab_to_close(const std::weak_ptr& tabToClose); bool network_being_used(); bool net_server_hosted(); void update_display_names(); @@ -100,9 +100,11 @@ class MainProgram { void set_vsync_value(int vsyncValue); void update_scale_and_density(); float get_scale_and_density_factor_gui(); + bool app_close_requested(); std::filesystem::path homePath; std::filesystem::path configPath; + std::filesystem::path documentsPath; bool drawGui = true; bool takingScreenshot = false; @@ -121,7 +123,7 @@ class MainProgram { std::atomic tabSetToOpen = false; World::OpenWorldInfo newTabToOpenInfo; - std::optional setTabToClose; + std::vector> setTabsToClose; std::string gen_random_display_name(); diff --git a/src/Toolbar.cpp b/src/Toolbar.cpp index 7c3d7a5..a2830a0 100644 --- a/src/Toolbar.cpp +++ b/src/Toolbar.cpp @@ -375,6 +375,8 @@ void Toolbar::update() { top_toolbar(); if(!main.world->clientStillConnecting) drawing_program_gui(); + if(!closePopupData.worldsToClose.empty()) + close_popup_gui(); if(viewWebVersionWelcome) web_version_welcome(); #ifndef __EMSCRIPTEN__ @@ -423,11 +425,110 @@ void Toolbar::update() { calculate_final_gui_scale(); } +bool Toolbar::app_close_requested() { + for(auto& w : main.worlds) { + if(w->should_ask_before_closing()) + add_world_to_close_popup_data(w); + } + closePopupData.closeAppWhenDone = true; + return closePopupData.worldsToClose.empty(); +} + +void Toolbar::add_world_to_close_popup_data(const std::shared_ptr& w) { + auto it = std::find_if(closePopupData.worldsToClose.begin(), closePopupData.worldsToClose.end(), [&](auto& wStruct) { + return wStruct.w.lock() == w; + }); + if(it == closePopupData.worldsToClose.end()) + closePopupData.worldsToClose.emplace_back(w, true); +} + +void Toolbar::close_popup_gui() { + std::erase_if(closePopupData.worldsToClose, [](auto& wPair) { + return wPair.w.expired(); + }); + CLAY_AUTO_ID({ + .layout = { + .sizing = {.width = CLAY_SIZING_FIT(0), .height = CLAY_SIZING_FIT(0) }, + .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 = {.attachPoints = {.element = CLAY_ATTACH_POINT_CENTER_CENTER, .parent = CLAY_ATTACH_POINT_CENTER_CENTER}, .attachTo = CLAY_ATTACH_TO_PARENT} + }) { + gui.obstructing_window(); + gui.push_id("Close single file popup gui"); + gui.text_label("Files may contain unsaved changes"); + size_t i = 0; + gui.push_id("File list"); + for(auto& [w, setToSave] : closePopupData.worldsToClose) { + auto wLock = w.lock(); + CLAY_AUTO_ID({ + .layout = { + .sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_FIT(0) }, + .padding = CLAY_PADDING_ALL(io->theme->padding1), + .childGap = io->theme->childGap1, + .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER}, + .layoutDirection = CLAY_LEFT_TO_RIGHT + }, + .backgroundColor = convert_vec4(io->theme->backColor2), + .cornerRadius = CLAY_CORNER_RADIUS(io->theme->windowCorners1), + }) { + gui.push_id(i); + gui.checkbox("set to save checkbox", &setToSave); + CLAY_AUTO_ID({ + .layout = { + .sizing = {.width = CLAY_SIZING_FIT(0), .height = CLAY_SIZING_FIT(0) }, + .childGap = 0, + .childAlignment = { .x = CLAY_ALIGN_X_LEFT, .y = CLAY_ALIGN_Y_CENTER}, + .layoutDirection = CLAY_TOP_TO_BOTTOM + } + }) { + gui.text_label(wLock->name); + gui.text_label_light(wLock->filePath.empty() ? "Autosave in " + main.documentsPath.string() : wLock->filePath.string()); + } + gui.pop_id(); + } + ++i; + } + gui.pop_id(); + if(gui.text_button_wide("Save", "Save")) { + for(auto& [w, setToSave] : closePopupData.worldsToClose) { + auto wLock = w.lock(); + if(setToSave) { + if(wLock->filePath.empty()) + wLock->autosave_to_directory(main.documentsPath); + else + wLock->save_to_file(wLock->filePath); + } + main.set_tab_to_close(w); + } + closePopupData.worldsToClose.clear(); + if(closePopupData.closeAppWhenDone) + main.setToQuit = true; + } + if(gui.text_button_wide("Discard All", "Discard All")) { + for(auto& [w, setToSave] : closePopupData.worldsToClose) + main.set_tab_to_close(w); + closePopupData.worldsToClose.clear(); + if(closePopupData.closeAppWhenDone) + main.setToQuit = true; + } + if(gui.text_button_wide("Cancel", "Cancel")) { + closePopupData.worldsToClose.clear(); + closePopupData.closeAppWhenDone = false; + } + gui.pop_id(); + } +} + void Toolbar::save_func() { if(main.world->filePath == std::filesystem::path()) save_as_func(); else - main.world->save_to_file(main.world->filePath.string()); + main.world->save_to_file(main.world->filePath); } void Toolbar::save_as_func() { @@ -484,10 +585,22 @@ void Toolbar::top_toolbar() { } } std::vector> tabNames; - for(size_t i = 0; i < main.worlds.size(); i++) - tabNames.emplace_back(main.worlds[i]->netObjMan.is_connected() ? "data/icons/network.svg" : "", main.worlds[i]->name + (main.worlds[i]->hasUnsavedLocalChanges ? "*" : "")); + for(size_t i = 0; i < main.worlds.size(); i++) { + auto& w = main.worlds[i]; + bool shouldAddStarNextToName = w->should_ask_before_closing() && !w->netServer && !w->netClient; + tabNames.emplace_back(w->netObjMan.is_connected() ? "data/icons/network.svg" : "", w->name + (shouldAddStarNextToName ? "*" : "")); + } + std::optional closedTab; gui.tab_list("file tab list", tabNames, main.worldIndex, closedTab); + if(closedTab) { + auto& closedWorldPtr = main.worlds[closedTab.value()]; + if(closedWorldPtr->should_ask_before_closing()) + add_world_to_close_popup_data(closedWorldPtr); + else + main.set_tab_to_close(closedWorldPtr); + } + if(!main.world->clientStillConnecting) { if(main.world->netObjMan.is_connected() && gui.svg_icon_button_transparent("Player List Toggle Button", "data/icons/list.svg", playerMenuOpen)) playerMenuOpen = !playerMenuOpen; @@ -524,9 +637,6 @@ void Toolbar::top_toolbar() { } } - - if(closedTab) - main.set_tab_to_close(closedTab.value()); if(gridMenu.popupOpen) grid_menu(gridMenuPopUpJustOpen); if(bookmarkMenuPopupOpen) diff --git a/src/Toolbar.hpp b/src/Toolbar.hpp index bea1907..1cbc22d 100644 --- a/src/Toolbar.hpp +++ b/src/Toolbar.hpp @@ -90,6 +90,8 @@ class Toolbar { bool flipZoomToolDirection = false; float final_gui_scale(); + + bool app_close_requested(); private: void calculate_final_gui_scale(); float final_gui_scale_not_fit(); @@ -117,6 +119,17 @@ class Toolbar { void web_version_welcome(); void still_connecting_center_message(); void no_layers_being_edited_message(); + void close_popup_gui(); + void add_world_to_close_popup_data(const std::shared_ptr& w); + + struct ClosePopupData { + struct CloseWorldData { + std::weak_ptr w; + bool setToSave = true; + }; + std::vector worldsToClose; // Using a vector to ensure that the worlds are in proper order + bool closeAppWhenDone = false; + } closePopupData; #ifndef __EMSCRIPTEN__ void update_notification_gui(); diff --git a/src/World.cpp b/src/World.cpp index 0ba4851..1adc677 100644 --- a/src/World.cpp +++ b/src/World.cpp @@ -13,6 +13,7 @@ #include "Helpers/NetworkingObjects/NetObjTemporaryPtr.decl.hpp" #include "Helpers/NetworkingObjects/NetObjUnorderedSet.hpp" #include "CommandList.hpp" +#include "Helpers/StringHelpers.hpp" #include "MainProgram.hpp" #include "SharedTypes.hpp" #include "Toolbar.hpp" @@ -275,42 +276,10 @@ void World::set_name(const std::string& n) { } void World::ensure_display_name_unique(std::string& displayName) { - for(;;) { - bool isUnique = true; - for(auto& client : clients->get_data()) { - if(client->get_display_name() == displayName) { - size_t leftParenthesisIndex = displayName.find_last_of('('); - size_t rightParenthesisIndex = displayName.find_last_of(')'); - bool failToIncrement = true; - if(leftParenthesisIndex != std::string::npos && rightParenthesisIndex != std::string::npos && leftParenthesisIndex < rightParenthesisIndex) { - std::string numStr = displayName.substr(leftParenthesisIndex + 1, rightParenthesisIndex - leftParenthesisIndex - 1); - bool isAllDigits = true; - for(char c : numStr) { - if(!isdigit(c)) { - isAllDigits = false; - break; - } - } - if(isAllDigits) { - try { - int s = std::stoi(numStr); - s++; - displayName = displayName.substr(0, leftParenthesisIndex); - displayName += "(" + std::to_string(s) + ")"; - failToIncrement = false; - } - catch(...) {} - } - } - if(failToIncrement) - displayName += " (2)"; - isUnique = false; - break; - } - } - if(isUnique) - break; - } + std::vector strList; + for(auto& client : clients->get_data()) + strList.emplace_back(client->get_display_name()); + displayName = ensure_string_unique(strList, displayName); } void World::start_hosting(const std::string& initNetSource, const std::string& serverLocalID) { @@ -368,6 +337,16 @@ void World::start_hosting(const std::string& initNetSource, const std::string& s }); } +void World::autosave_to_directory(const std::filesystem::path& directoryToSaveAt) { + std::vector strList; + for(auto& entry : std::filesystem::directory_iterator(directoryToSaveAt)) { + if(entry.path().has_stem() && entry.path().has_extension() && entry.path().extension() == ("." + FILE_EXTENSION)) + strList.emplace_back(entry.path().stem()); + } + std::string nameToSaveUnder = ensure_string_unique(strList, name); + save_to_file(directoryToSaveAt / std::filesystem::path(nameToSaveUnder + "." + FILE_EXTENSION)); +} + void World::save_to_file(const std::filesystem::path& filePathToSaveAt) { try { filePath = force_extension_on_path(filePathToSaveAt, FILE_EXTENSION); @@ -505,6 +484,20 @@ void World::scale_up(const WorldScalar& scaleUpAmount) { undo.scale_up(scaleUpAmount); } +bool World::should_ask_before_closing() { + // If it's a client that didn't save to a file previously, we can ignore asking before quitting (probably don't intend to save anyway) + // If it's a server or a client, and the previous statement is false, we should always ask before quitting because the data could be modified by someone on the network + // If it's a local file, ask before quitting if we detect any local changes + // If on web version, don't ask before closing +#ifdef __EMCSRIPTEN__ + return false; +#else + if(netClient || netServer) + return !(netClient && filePath.empty()); + return hasUnsavedLocalChanges; +#endif +} + #ifdef ENABLE_ORDERED_LIST_TEST void World::list_debug_test_update() { if(std::chrono::steady_clock::now() < (listDebugTestTimeStart + std::chrono::minutes(1))) { diff --git a/src/World.hpp b/src/World.hpp index 2b8d954..954674e 100644 --- a/src/World.hpp +++ b/src/World.hpp @@ -72,6 +72,7 @@ class World { bool clientStillConnecting = false; + void autosave_to_directory(const std::filesystem::path& directoryToSaveAt); void save_to_file(const std::filesystem::path& filePathToSaveAt); void load_from_file(const std::filesystem::path& filePathToLoadFrom, std::string_view buffer); @@ -89,6 +90,8 @@ class World { void scale_up(const WorldScalar& scaleUpAmount); void scale_up_step(); + bool should_ask_before_closing(); + bool hasUnsavedLocalChanges = false; bool setToDestroy = false; NetworkingObjects::DelayUpdateSerializedClassManager delayedUpdateObjectManager; diff --git a/src/main.cpp b/src/main.cpp index 0cd53f4..2bafa8a 100755 --- a/src/main.cpp +++ b/src/main.cpp @@ -437,6 +437,15 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) { mS.m->logFile = &mS.logFile; mS.m->configPath = mS.configPath; mS.m->homePath = mS.homePath; +#ifndef __EMSCRIPTEN__ + mS.m->documentsPath = sago::getDocumentsFolder(); + if(!std::filesystem::exists(mS.m->documentsPath) || !std::filesystem::is_directory(mS.m->documentsPath)) { + mS.m->documentsPath = sago::getDesktopFolder(); + if(!std::filesystem::exists(mS.m->documentsPath) || !std::filesystem::is_directory(mS.m->documentsPath)) { + mS.m->documentsPath = mS.m->configPath; + } + } +#endif mS.m->window.sdlWindow = mS.window; mS.m->update_scale_and_density(); #ifdef __EMSCRIPTEN__ @@ -626,11 +635,15 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { #endif switch(event->type) { case SDL_EVENT_QUIT: - return SDL_APP_SUCCESS; - case SDL_EVENT_WINDOW_CLOSE_REQUESTED: - if(event->window.windowID == SDL_GetWindowID(mS.window)) + if(mS.m->app_close_requested()) return SDL_APP_SUCCESS; break; + case SDL_EVENT_WINDOW_CLOSE_REQUESTED: + if(event->window.windowID == SDL_GetWindowID(mS.window)) { + if(mS.m->app_close_requested()) + return SDL_APP_SUCCESS; + } + break; case SDL_EVENT_WINDOW_MOVED: mS.m->window.pos.x() = event->window.data1; mS.m->window.pos.y() = event->window.data2;