diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 22ff9b6b15..585a37c608 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -56,12 +56,18 @@ set(COMMON_SRC "${CMAKE_CURRENT_LIST_DIR}/reflectivity/reflectivity.cpp" ) - set(UTILITIES + set(UTILITIES_FILES "${CMAKE_CURRENT_LIST_DIR}/utilities/number/stabilized-value.h" + "${CMAKE_CURRENT_LIST_DIR}/utilities/string/trim-newlines.h" + "${CMAKE_CURRENT_LIST_DIR}/utilities/string/split.h" + "${CMAKE_CURRENT_LIST_DIR}/utilities/imgui/wrap.h" + "${CMAKE_CURRENT_LIST_DIR}/utilities/imgui/wrap.cpp" ) + set(COMMON_SRC ${COMMON_SRC} ${SW_UPDATE_FILES} ${REFLECTIVITY_FILES} - ${UTILITIES}) + ${UTILITIES_FILES} + ) diff --git a/common/utilities/imgui/wrap.cpp b/common/utilities/imgui/wrap.cpp new file mode 100644 index 0000000000..1d447e78d1 --- /dev/null +++ b/common/utilities/imgui/wrap.cpp @@ -0,0 +1,122 @@ +// License: Apache 2.0. See LICENSE file in root directory. +// Copyright(c) 2020 Intel Corporation. All Rights Reserved. + +#include +#include +#include +#include "wrap.h" +#include "../common/utilities/string/split.h" +#include "../third-party/imgui/imgui.h" + +namespace utilities { +namespace imgui { + +void trim_leading_spaces( std::string &remaining_paragraph ) +{ + auto non_space_index = remaining_paragraph.find_first_not_of( ' ' ); + + if( non_space_index != std::string::npos ) + { + // Remove trailing spaces + remaining_paragraph = remaining_paragraph.substr( non_space_index ); + } +} + +std::string wrap_paragraph( const std::string & paragraph, int wrap_pixels_width ) +{ + float space_width = ImGui::CalcTextSize( " " ).x; // Calculate space width in pixels + std::string wrapped_line; // The line that is wrapped in this iteration + std::string wrapped_paragraph; // The output wrapped paragraph + auto remaining_paragraph + = paragraph; // Holds the remaining unwrapped part of the input paragraph + + // Handle a case when the paragraph starts with spaces + trim_leading_spaces(remaining_paragraph); + + auto next_word = remaining_paragraph.substr( + 0, + remaining_paragraph.find( ' ' ) ); // The next word to add to the current line + bool first_word = true; + + while( ! next_word.empty() ) + { + float next_x = 0.0f; + // If this is the first word we try to place it first in line, + // if not we concatenate it to the last word after adding a space + if( ! first_word ) + { + next_x = ImGui::CalcTextSize( wrapped_line.c_str() ).x + space_width; + } + + if( next_x + ImGui::CalcTextSize( next_word.c_str() ).x <= wrap_pixels_width ) + { + if( ! first_word ) + wrapped_line += " "; // First word should not start with " " + wrapped_line += next_word; + } + else + { // Current line cannot feat new word so we wrap the line and + // start building the new line + + wrapped_paragraph += wrapped_line; // copy line build by now + if( ! first_word ) + wrapped_paragraph += '\n'; // break the previous line if exist + wrapped_line = next_word; // add next work to new line + } + + first_word = false; + + // If we have more characters left other then the current word, prepare inputs for next + // iteration, If not add the current line built so far to the output and finish current + // line wrap. + if( remaining_paragraph.size() > next_word.size() ) + { + remaining_paragraph = remaining_paragraph.substr( next_word.size() + 1 ); + + // Handle a case when the paragraph starts with spaces + trim_leading_spaces(remaining_paragraph); + + next_word = remaining_paragraph.substr( 0, remaining_paragraph.find( ' ' ) ); + + // If no more words exist, copy the current wrapped line to output and stop + if( next_word.empty() ) + { + wrapped_paragraph += wrapped_line; + break; + } + } + else + { + wrapped_paragraph += wrapped_line; + break; + } + } + + return wrapped_paragraph; +} + +std::string wrap( const std::string & text, int wrap_pixels_width ) +{ + // Do not wrap if the wrap is smaller then 32 pixels (~2 characters on font 16) + if( wrap_pixels_width < 32 ) + return text; + + // Split text into paragraphs + auto paragraphs_vector = string::split( text, '\n' ); + + std::string wrapped_text; + size_t line_number = 1; + // Wrap each line according to the requested wrap width + for( auto paragraph : paragraphs_vector ) + { + wrapped_text += wrap_paragraph( paragraph, wrap_pixels_width ); + // Each paragraph except the last one ends with a new line + if( line_number++ != paragraphs_vector.size() ) + wrapped_text += '\n'; + } + + return wrapped_text; +} + +} // namespace imgui +} // namespace utilities diff --git a/common/utilities/imgui/wrap.h b/common/utilities/imgui/wrap.h new file mode 100644 index 0000000000..4afd43b988 --- /dev/null +++ b/common/utilities/imgui/wrap.h @@ -0,0 +1,23 @@ +// License: Apache 2.0. See LICENSE file in root directory. +// Copyright(c) 2020 Intel Corporation. All Rights Reserved. + +#pragma once + +namespace utilities { +namespace imgui { + +// Wrap text according to input width +// - Input: - text as a string +// - wrapping width +// - Output: - on success - wrapped text +// - on failure - an empty string +// Example: +// Input: +// this is the first line\nthis is the second line\nthis is the last line , wrap_width = 150 [pixels] +// Output: +// this is the\nfirst line\nthis is the\nsecond line\nthis is the last\nline +// Note: If the paragraph contain multiple spaces, it will be trimmed into a single space. +std::string wrap( const std::string & text, int wrap_pixels_width ); + +} // namespace imgui +} // namespace utilities diff --git a/common/utilities/string/split.h b/common/utilities/string/split.h new file mode 100644 index 0000000000..2386588a03 --- /dev/null +++ b/common/utilities/string/split.h @@ -0,0 +1,38 @@ +// License: Apache 2.0. See LICENSE file in root directory. +// Copyright(c) 2020 Intel Corporation. All Rights Reserved. + +#pragma once + +#include +#include +#include + +namespace utilities { +namespace string { + + +// Split input text to vector of strings according to input delimiter +// - Input: string to be split +// - delimiter +// - Output: a vector of strings +// Example: +// Input: +// Text: This is the first line\nThis is the second line\nThis is the last line +// Delimiter : '\n' +// Output: +// [0] this is the first line +// [1] this is the second line +// [2] this is the last line +inline std::vector< std::string > split( const std::string & str , char delimiter) +{ + auto result = std::vector< std::string >{}; + auto ss = std::stringstream{ str }; + + for( std::string line; std::getline( ss, line, delimiter); ) + result.push_back( line ); + + return result; +} + +} // namespace string +} // namespace utilities diff --git a/common/viewer.cpp b/common/viewer.cpp index 81c6bd5cbe..b87d63b35c 100644 --- a/common/viewer.cpp +++ b/common/viewer.cpp @@ -21,6 +21,8 @@ #define ARCBALL_CAMERA_IMPLEMENTATION #include +#include "../common/utilities/string/trim-newlines.h" +#include "../common/utilities/imgui/wrap.h" namespace rs2 { @@ -1068,9 +1070,24 @@ namespace rs2 auto custom_command = [&]() { auto msg = _active_popups.front().message; + + // Wrap the text to feet the error pop-up window + std::string wrapped_msg; + try + { + auto trimmed_msg = utilities::string::trim_newlines(msg); + wrapped_msg = utilities::imgui::wrap(trimmed_msg, 500); + } + catch (...) + { + wrapped_msg = msg; // Revert to original text on wrapping failure + not_model->output.add_log(RS2_LOG_SEVERITY_WARN, __FILE__, __LINE__, + to_string() << "Wrapping of error message text failed!"); + } + ImGui::Text("RealSense error calling:"); ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, regular_blue); - ImGui::InputTextMultiline("##error", const_cast(msg.c_str()), + ImGui::InputTextMultiline("##error", const_cast(wrapped_msg.c_str()), msg.size() + 1, { 500,95 }, ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_ReadOnly); ImGui::PopStyleColor(); diff --git a/unit-tests/utilities/imgui/common.h b/unit-tests/utilities/imgui/common.h new file mode 100644 index 0000000000..6274e5a819 --- /dev/null +++ b/unit-tests/utilities/imgui/common.h @@ -0,0 +1,15 @@ +// License: Apache 2.0. See LICENSE file in root directory. +// Copyright(c) 2020 Intel Corporation. All Rights Reserved. + +#define CATCH_CONFIG_MAIN + +#include "../../catch.h" + +#include +#ifdef BUILD_SHARED_LIBS +// With static linkage, ELPP is initialized by librealsense, so doing it here will +// create errors. When we're using the shared .so/.dll, the two are separate and we have +// to initialize ours if we want to use the APIs! +INITIALIZE_EASYLOGGINGPP +#endif + diff --git a/unit-tests/utilities/imgui/test-wrap.cpp b/unit-tests/utilities/imgui/test-wrap.cpp new file mode 100644 index 0000000000..74b5a35328 --- /dev/null +++ b/unit-tests/utilities/imgui/test-wrap.cpp @@ -0,0 +1,90 @@ +// License: Apache 2.0. See LICENSE file in root directory. +// Copyright(c) 2020 Intel Corporation. All Rights Reserved. + +//#cmake:add-file ../../../common/utilities/imgui/wrap.h +//#cmake:add-file ../../../common/utilities/imgui/wrap.cpp +//#cmake:add-file ../../../third-party/imgui/imgui.h + +#include "common.h" +#include "../../../common/utilities/imgui/wrap.h" +#include "../../../third-party/imgui/imgui.h" + +namespace ImGui { +// Mock ImGui function for test +// all characters are at length of 10 pixels except 'i' and 'j' which is 5 pixels +ImVec2 CalcTextSize( const char * text, + const char * text_end, + bool hide_text_after_double_hash, + float wrap_width ) +{ + if (!text) return ImVec2(0.0f, 0.0f); + + float total_size = 0.0f; + while( *text ) + { + if( *text == 'i' || *text == 'j' ) + total_size += 5.0f; + else + total_size += 10.0f; + text++; + } + return ImVec2( total_size, 0.0f ); +} +} // namespace ImGui + +using namespace utilities::imgui; + +TEST_CASE( "wrap-text", "[string]" ) +{ + // Verify illegal inputs return empty string + CHECK( wrap( "", 0 ) == "" ); + CHECK( wrap( "", 10 ) == "" ); + CHECK( wrap( "abc", 0 ) == "abc" ); + CHECK( wrap( "abc", 10 ) == "abc" ); + CHECK( wrap( "abc\nabc", 0 ) == "abc\nabc" ); + CHECK( wrap( "abc abc", 5 ) == "abc abc" ); + CHECK( wrap( "", 10 ) == "" ); + + // Verify no wrap if not needed + CHECK( wrap( "abc", 100 ) == "abc" ); + CHECK( wrap( "abc\nabc", 100 ) == "abc\nabc" ); + CHECK( wrap( "abc abc a", 100 ) == "abc abc a" ); + + // No wrapping possible, copy line until first space and continue wrapping + CHECK( wrap( "abcdefgh", 40 ) == "abcdefgh" ); + CHECK( wrap( "aabbccddff das ds fr", 50 ) == "aabbccddff\ndas\nds fr" ); + CHECK( wrap( "das aabbccddff ds fr", 50 ) == "das\naabbccddff\nds fr" ); + + // Exact wrap position test + CHECK( wrap( "abcde abcde", 50 ) == "abcde\nabcde" ); + + // Short letters test + CHECK(wrap("aaaa bbbb cc", 100) == "aaaa bbbb\ncc"); + // i and j are only 5 pixels so we get more characters inside the wrap + CHECK(wrap("aaaa iijj cc", 100) == "aaaa iijj cc"); + + + // Check wrapping of 3 paragraphs + CHECK( wrap( "this is the first line\nthis is the second line\nthis is the last line", 150 ) + == "this is the\nfirst line\nthis is the\nsecond line\nthis is the last\nline" ); + + CHECK( wrap( "this is the first line\nthis is the second line\nthis is the last line", 60 ) + == "this is\nthe\nfirst\nline\nthis is\nthe\nsecond\nline\nthis is\nthe\nlast\nline" ); + + // Spaces checks + CHECK(wrap("ab cd ", 32) == "ab\ncd"); // Ending spaces + CHECK(wrap("ab cd", 32) == "ab\ncd"); // Middle spaces + CHECK(wrap(" ab cd ", 32) == "ab\ncd"); // Mixed multiple spaces + CHECK(wrap(" ab cd ", 32) == "ab\ncd"); // Mixed multiple spaces + CHECK(wrap(" ab ", 33) == "ab"); // Mixed multiple spaces + CHECK(wrap("ab ", 33) == "ab"); // Ending multiple spaces + CHECK(wrap(" ab", 33) == "ab"); + + // Only spaces + CHECK(wrap(" ", 33) == ""); + CHECK(wrap(" ", 33) == ""); + + CHECK(wrap("ab cd ", 100) == "ab cd"); + CHECK(wrap("ab cd", 100) == "ab cd"); // Known corner case - we trim multiple spaces + +} diff --git a/unit-tests/utilities/string/test-split.cpp b/unit-tests/utilities/string/test-split.cpp new file mode 100644 index 0000000000..8c14cfee36 --- /dev/null +++ b/unit-tests/utilities/string/test-split.cpp @@ -0,0 +1,31 @@ +// License: Apache 2.0. See LICENSE file in root directory. +// Copyright(c) 2020 Intel Corporation. All Rights Reserved. + +//#cmake:add-file ../../../common/utilities/string/split.h + +#include "common.h" +#include "../../../common/utilities/string/split.h" + +using namespace utilities::string; + +TEST_CASE("split_string_by_newline", "[string]") +{ + // split size check + CHECK(split("" , '\n').size() == 0); + CHECK(split("abc", '\n').size() == 1); + CHECK(split("abc\nabc", '\n').size() == 2); + CHECK(split("a\nbc\nabc", '\n').size() == 3); + CHECK(split("a\nbc\nabc\n", '\n').size() == 3); + CHECK(split("1-12-123-1234", '-').size() == 4); + + CHECK(split("a\nbc\nabc", '\n')[0] == "a"); + CHECK(split("a\nbc\nabc", '\n')[1] == "bc"); + CHECK(split("a\nbc\nabc", '\n')[2] == "abc"); + CHECK(split("a\nbc\nabc\n", '\n')[2] == "abc"); + + + CHECK(split("1-12-123-1234", '-')[0] == "1"); + CHECK(split("1-12-123-1234", '-')[1] == "12"); + CHECK(split("1-12-123-1234", '-')[2] == "123"); + CHECK(split("1-12-123-1234", '-')[3] == "1234"); +}