diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 01:24:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 01:24:41 +0000 |
commit | a9bcc81f821d7c66f623779fa5147e728eb3c388 (patch) | |
tree | 98676963bcdd537ae5908a067a8eb110b93486a6 /client/SDL/dialogs | |
parent | Initial commit. (diff) | |
download | freerdp3-a9bcc81f821d7c66f623779fa5147e728eb3c388.tar.xz freerdp3-a9bcc81f821d7c66f623779fa5147e728eb3c388.zip |
Adding upstream version 3.3.0+dfsg1.upstream/3.3.0+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'client/SDL/dialogs')
31 files changed, 3761 insertions, 0 deletions
diff --git a/client/SDL/dialogs/CMakeLists.txt b/client/SDL/dialogs/CMakeLists.txt new file mode 100644 index 0000000..4cf2a16 --- /dev/null +++ b/client/SDL/dialogs/CMakeLists.txt @@ -0,0 +1,75 @@ +set(SRCS + sdl_button.hpp + sdl_button.cpp + sdl_buttons.hpp + sdl_buttons.cpp + sdl_dialogs.cpp + sdl_dialogs.hpp + sdl_widget.hpp + sdl_widget.cpp + sdl_input.hpp + sdl_input.cpp + sdl_input_widgets.cpp + sdl_input_widgets.hpp + sdl_select.hpp + sdl_select.cpp + sdl_selectlist.hpp + sdl_selectlist.cpp + sdl_connection_dialog.cpp + sdl_connection_dialog.hpp +) + +list(APPEND LIBS + sdl_client_res + winpr +) + +if (NOT WITH_SDL_LINK_SHARED) + list(APPEND LIBS ${SDL2_STATIC_LIBRARIES}) +else() + list(APPEND LIBS ${SDL2_LIBRARIES}) +endif() + +macro(find_sdl_component name) + find_package(${name}) + if (NOT ${name}_FOUND) + find_package(PkgConfig REQUIRED) + pkg_check_modules(${name} REQUIRED ${name}) + + if (BUILD_SHARED_LIBS) + list(APPEND LIBS ${${name}_LIBRARIES}) + link_directories(${${name}_LIBRARY_DIRS}) + include_directories(${${name}_INCLUDE_DIRS}) + else() + list(APPEND LIBS ${${name}_STATIC_LIBRARIES}) + link_directories(${${name}_STATIC_LIBRARY_DIRS}) + include_directories(${${name}_STATIC_INCLUDE_DIRS}) + endif() + else() + if (WITH_SDL_LINK_SHARED) + list(APPEND LIBS ${name}::${name}) + else() + list(APPEND LIBS ${name}::${name}-static) + endif() + endif() +endmacro() + +find_sdl_component(SDL2_ttf) + +option(WITH_SDL_IMAGE_DIALOGS "Build with SDL_image support (recommended)" OFF) +if (WITH_SDL_IMAGE_DIALOGS) + find_sdl_component(SDL2_image) + add_definitions(-DWITH_SDL_IMAGE_DIALOGS) +endif() + +add_subdirectory(res) + +add_library(dialogs STATIC + ${SRCS} +) + +target_link_libraries(dialogs PRIVATE ${LIBS}) + +if(BUILD_TESTING) +# add_subdirectory(test) +endif() diff --git a/client/SDL/dialogs/font/OFL.txt b/client/SDL/dialogs/font/OFL.txt new file mode 100644 index 0000000..9b448d4 --- /dev/null +++ b/client/SDL/dialogs/font/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/client/SDL/dialogs/font/OpenSans-Italic-VariableFont_wdth,wght.ttf b/client/SDL/dialogs/font/OpenSans-Italic-VariableFont_wdth,wght.ttf Binary files differnew file mode 100644 index 0000000..5bda9cc --- /dev/null +++ b/client/SDL/dialogs/font/OpenSans-Italic-VariableFont_wdth,wght.ttf diff --git a/client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttf b/client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttf Binary files differnew file mode 100644 index 0000000..e4142bf --- /dev/null +++ b/client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttf diff --git a/client/SDL/dialogs/font/README.txt b/client/SDL/dialogs/font/README.txt new file mode 100644 index 0000000..2548322 --- /dev/null +++ b/client/SDL/dialogs/font/README.txt @@ -0,0 +1,100 @@ +Open Sans Variable Font +======================= + +This download contains Open Sans as both variable fonts and static fonts. + +Open Sans is a variable font with these axes: + wdth + wght + +This means all the styles are contained in these files: + OpenSans-VariableFont_wdth,wght.ttf + OpenSans-Italic-VariableFont_wdth,wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Open Sans: + static/OpenSans_Condensed-Light.ttf + static/OpenSans_Condensed-Regular.ttf + static/OpenSans_Condensed-Medium.ttf + static/OpenSans_Condensed-SemiBold.ttf + static/OpenSans_Condensed-Bold.ttf + static/OpenSans_Condensed-ExtraBold.ttf + static/OpenSans_SemiCondensed-Light.ttf + static/OpenSans_SemiCondensed-Regular.ttf + static/OpenSans_SemiCondensed-Medium.ttf + static/OpenSans_SemiCondensed-SemiBold.ttf + static/OpenSans_SemiCondensed-Bold.ttf + static/OpenSans_SemiCondensed-ExtraBold.ttf + static/OpenSans-Light.ttf + static/OpenSans-Regular.ttf + static/OpenSans-Medium.ttf + static/OpenSans-SemiBold.ttf + static/OpenSans-Bold.ttf + static/OpenSans-ExtraBold.ttf + static/OpenSans_Condensed-LightItalic.ttf + static/OpenSans_Condensed-Italic.ttf + static/OpenSans_Condensed-MediumItalic.ttf + static/OpenSans_Condensed-SemiBoldItalic.ttf + static/OpenSans_Condensed-BoldItalic.ttf + static/OpenSans_Condensed-ExtraBoldItalic.ttf + static/OpenSans_SemiCondensed-LightItalic.ttf + static/OpenSans_SemiCondensed-Italic.ttf + static/OpenSans_SemiCondensed-MediumItalic.ttf + static/OpenSans_SemiCondensed-SemiBoldItalic.ttf + static/OpenSans_SemiCondensed-BoldItalic.ttf + static/OpenSans_SemiCondensed-ExtraBoldItalic.ttf + static/OpenSans-LightItalic.ttf + static/OpenSans-Italic.ttf + static/OpenSans-MediumItalic.ttf + static/OpenSans-SemiBoldItalic.ttf + static/OpenSans-BoldItalic.ttf + static/OpenSans-ExtraBoldItalic.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them in your products & projects – print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/client/SDL/dialogs/res/CMakeLists.txt b/client/SDL/dialogs/res/CMakeLists.txt new file mode 100644 index 0000000..5591e4a --- /dev/null +++ b/client/SDL/dialogs/res/CMakeLists.txt @@ -0,0 +1,89 @@ + +add_executable(freerdp-res2bin + convert_res_to_c.cpp +) + +set(SRCS + sdl_resource_manager.cpp + sdl_resource_manager.hpp +) + +set(RES_SVG_FILES + ${CMAKE_SOURCE_DIR}/resources/FreeRDP_Icon.svg + ${CMAKE_SOURCE_DIR}/resources/icon_info.svg + ${CMAKE_SOURCE_DIR}/resources/icon_warning.svg + ${CMAKE_SOURCE_DIR}/resources/icon_error.svg +) + +set(RES_FONT_FILES + ${CMAKE_SOURCE_DIR}/client/SDL/dialogs/font/OpenSans-VariableFont_wdth,wght.ttf +) + +macro(convert_to_bin FILE FILE_TYPE) + get_filename_component(FILE_NAME ${FILE} NAME) + string(REGEX REPLACE "[^a-zA-Z0-9]" "_" TARGET_NAME ${FILE_NAME}) + + set(FILE_BIN_DIR ${CMAKE_CURRENT_BINARY_DIR}/bin) + set(FILE_BYPRODUCTS ${FILE_BIN_DIR}/${TARGET_NAME}.hpp ${FILE_BIN_DIR}/${TARGET_NAME}.cpp) + + list(APPEND FACTORY_SRCS + ${FILE_BYPRODUCTS} + ) + + add_custom_command( + OUTPUT ${FILE_BYPRODUCTS} + COMMAND ${CMAKE_COMMAND} -E make_directory ${FILE_BIN_DIR} + COMMAND $<TARGET_FILE:freerdp-res2bin> ${FILE} ${FILE_TYPE} ${TARGET_NAME} ${FILE_BIN_DIR} + COMMENT "create image resources" + DEPENDS freerdp-res2bin + DEPENDS ${FILE} + ) +endmacro() + +option(SDL_USE_COMPILED_RESOURCES "Compile in images/fonts" ON) + +if (SDL_USE_COMPILED_RESOURCES) + list(APPEND SRCS + sdl_resource_file.cpp + sdl_resource_file.hpp + ) + + include_directories(${CMAKE_CURRENT_SOURCE_DIR}) + + if (WITH_SDL_IMAGE_DIALOGS) + foreach(FILE ${RES_SVG_FILES}) + convert_to_bin("${FILE}" "images") + endforeach() + endif() + + foreach(FILE ${RES_FONT_FILES}) + convert_to_bin("${FILE}" "fonts") + endforeach() + add_definitions(-DSDL_USE_COMPILED_RESOURCES) +else() + set(SDL_RESOURCE_ROOT ${CMAKE_INSTALL_FULL_DATAROOTDIR}/FreeRDP) + if (WITH_BINARY_VERSIONING) + string(APPEND SDL_RESOURCE_ROOT "${PROJECT_VERSION_MAJOR}") + endif() + + add_definitions(-DSDL_RESOURCE_ROOT="${SDL_RESOURCE_ROOT}") + + if (WITH_SDL_IMAGE_DIALOGS) + install( + FILES ${RES_SVG_FILES} + DESTINATION ${SDL_RESOURCE_ROOT}/images + ) + endif() + + install( + FILES ${RES_FONT_FILES} + DESTINATION ${SDL_RESOURCE_ROOT}/fonts + ) +endif() + +add_library(sdl_client_res OBJECT + ${RES_FILES} + ${SRCS} + ${FACTORY_SRCS} +) +set_property(TARGET sdl_client_res PROPERTY POSITION_INDEPENDENT_CODE ON) diff --git a/client/SDL/dialogs/res/convert_res_to_c.cpp b/client/SDL/dialogs/res/convert_res_to_c.cpp new file mode 100644 index 0000000..07309d5 --- /dev/null +++ b/client/SDL/dialogs/res/convert_res_to_c.cpp @@ -0,0 +1,184 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <string> +#include <fstream> +#include <algorithm> +#include <iomanip> +#include <iostream> +#if __has_include(<filesystem>) +#include <filesystem> +namespace fs = std::filesystem; +#elif __has_include(<experimental/filesystem>) +#include <experimental/filesystem> +namespace fs = std::experimental::filesystem; +#else +#error Could not find system header "<filesystem>" or "<experimental/filesystem>" +#endif + +static void usage(const char* prg) +{ + std::cerr << prg << " <file> <type> <class name> <dst path>" << std::endl; +} + +static int write_comment_header(std::ostream& out, const fs::path& prg, const std::string& fname) +{ + out << "/* AUTOGENERATED file, do not edit" << std::endl + << " *" << std::endl + << " * generated by '" << prg.filename() << "'" << std::endl + << " *" << std::endl + << " * contains the converted file '" << fname << "'" << std::endl + << " */" << std::endl + << std::endl; + return 0; +} + +static int write_cpp_header(std::ostream& out, const fs::path& prg, const fs::path& file, + const std::string& name, const std::string& type) +{ + auto fname = file.filename().string(); + auto rc = write_comment_header(out, prg, fname); + if (rc != 0) + return rc; + + out << "#include <vector>" << std::endl + << "#include \"" << name << ".hpp\"" << std::endl + << std::endl + << "std::string " << name << "::name() {" << std::endl + << "return \"" << fname << "\";" << std::endl + << "}" << std::endl + << "std::string " << name << "::type() {" << std::endl + << "return \"" << type << "\";" << std::endl + << "}" << std::endl + << std::endl + << "const SDLResourceFile " << name << "::_initializer(type(), name(), init());" + << std::endl + << std::endl + << "std::vector<unsigned char> " << name << "::init() {" << std::endl + << "static const unsigned char data[] = {" << std::endl; + + return 0; +} + +static int readwrite(std::ofstream& out, std::ifstream& ifs) +{ + size_t pos = 0; + char c = 0; + while (ifs.read(&c, 1) && ifs.good()) + { + unsigned val = c & 0xff; + out << "0x" << std::hex << std::setfill('0') << std::setw(2) << val; + if (ifs.peek() != EOF) + out << ","; + if ((pos++ % 16) == 15) + out << std::endl; + } + + return 0; +} + +static int write_cpp_trailer(std::ostream& out) +{ + out << std::endl; + out << "};" << std::endl; + out << std::endl; + out << "return std::vector<unsigned char>(data, data + sizeof(data));" << std::endl; + out << "}" << std::endl; + return 0; +} + +static int write_hpp_header(const fs::path& prg, const fs::path& file, const std::string& name, + const std::string& fname) +{ + std::ofstream out(file, std::ios::out); + if (!out.is_open()) + { + std::cerr << "Failed to open output file '" << file << "'" << std::endl; + return -1; + } + auto rc = write_comment_header(out, prg, fname); + if (rc != 0) + return rc; + + out << "#pragma once" << std::endl + << std::endl + << "#include <vector>" << std::endl + << "#include <string>" << std::endl + << "#include \"sdl_resource_file.hpp\"" << std::endl + << std::endl + << "class " << name << " {" << std::endl + << "public:" << std::endl + << name << "() = delete;" << std::endl + << std::endl + << "static std::string name();" << std::endl + << "static std::string type();" << std::endl + << std::endl + << "private:" << std::endl + << "static std::vector<unsigned char> init();" << std::endl + << "static const SDLResourceFile _initializer;" << std::endl + << std::endl + << "};" << std::endl; + return 0; +} + +int main(int argc, char* argv[]) +{ + fs::path prg(argv[0]); + if (argc != 5) + { + usage(argv[0]); + return -1; + } + + fs::path file(argv[1]); + std::string etype = argv[2]; + std::string cname = argv[3]; + fs::path dst(argv[4]); + fs::path hdr(argv[4]); + + dst /= cname + ".cpp"; + hdr /= cname + ".hpp"; + + std::ofstream out; + out.open(dst); + if (!out.is_open()) + { + std::cerr << "Failed to open output file '" << dst << "'" << std::endl; + return -2; + } + + std::ifstream ifs(file, std::ios::in | std::ios::binary); + if (!ifs.is_open()) + { + std::cerr << "Failed to open input file '" << file << "'" << std::endl; + return -3; + } + + auto rc = write_cpp_header(out, prg, file, cname, etype); + if (rc != 0) + return -1; + + rc = readwrite(out, ifs); + if (rc != 0) + return rc; + + rc = write_cpp_trailer(out); + if (rc != 0) + return rc; + return write_hpp_header(prg, hdr, cname, file.filename().string()); +} diff --git a/client/SDL/dialogs/res/sdl_resource_file.cpp b/client/SDL/dialogs/res/sdl_resource_file.cpp new file mode 100644 index 0000000..c48612d --- /dev/null +++ b/client/SDL/dialogs/res/sdl_resource_file.cpp @@ -0,0 +1,25 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "sdl_resource_file.hpp" +#include "sdl_resource_manager.hpp" + +SDLResourceFile::SDLResourceFile(const std::string& type, const std::string& id, + const std::vector<unsigned char>& data) +{ + SDLResourceManager::insert(type, id, data); +} diff --git a/client/SDL/dialogs/res/sdl_resource_file.hpp b/client/SDL/dialogs/res/sdl_resource_file.hpp new file mode 100644 index 0000000..5846921 --- /dev/null +++ b/client/SDL/dialogs/res/sdl_resource_file.hpp @@ -0,0 +1,33 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <string> +#include <vector> + +class SDLResourceFile +{ + public: + SDLResourceFile(const std::string& type, const std::string& id, + const std::vector<unsigned char>& data); + virtual ~SDLResourceFile() = default; + + private: + SDLResourceFile(const SDLResourceFile& other) = delete; + SDLResourceFile(const SDLResourceFile&& other) = delete; +}; diff --git a/client/SDL/dialogs/res/sdl_resource_manager.cpp b/client/SDL/dialogs/res/sdl_resource_manager.cpp new file mode 100644 index 0000000..90ccf31 --- /dev/null +++ b/client/SDL/dialogs/res/sdl_resource_manager.cpp @@ -0,0 +1,78 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "sdl_resource_manager.hpp" +#include <iostream> +#if __has_include(<filesystem>) +#include <filesystem> +namespace fs = std::filesystem; +#elif __has_include(<experimental/filesystem>) +#include <experimental/filesystem> +namespace fs = std::experimental::filesystem; +#else +#error Could not find system header "<filesystem>" or "<experimental/filesystem>" +#endif + +SDL_RWops* SDLResourceManager::get(const std::string& type, const std::string& id) +{ + std::string uuid = type + "/" + id; + +#if defined(SDL_USE_COMPILED_RESOURCES) + auto val = resources().find(uuid); + if (val == resources().end()) + return nullptr; + + const auto& v = val->second; + return SDL_RWFromConstMem(v.data(), v.size()); +#else + fs::path path(SDL_RESOURCE_ROOT); + path /= type; + path /= id; + + if (!fs::exists(path)) + { + std::cerr << "sdl-freerdp expects resource '" << uuid << "' at location " + << fs::absolute(path) << std::endl; + std::cerr << "file not found, application will fail" << std::endl; + } + return SDL_RWFromFile(path.native().c_str(), "rb"); +#endif +} + +const std::string SDLResourceManager::typeFonts() +{ + return "fonts"; +} + +const std::string SDLResourceManager::typeImages() +{ + return "images"; +} + +void SDLResourceManager::insert(const std::string& type, const std::string& id, + const std::vector<unsigned char>& data) +{ + std::string uuid = type + "/" + id; + resources().emplace(uuid, data); +} + +std::map<std::string, std::vector<unsigned char>>& SDLResourceManager::resources() +{ + + static std::map<std::string, std::vector<unsigned char>> resources = {}; + return resources; +} diff --git a/client/SDL/dialogs/res/sdl_resource_manager.hpp b/client/SDL/dialogs/res/sdl_resource_manager.hpp new file mode 100644 index 0000000..b4f463c --- /dev/null +++ b/client/SDL/dialogs/res/sdl_resource_manager.hpp @@ -0,0 +1,46 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <string> +#include <map> +#include <vector> +#include <SDL.h> + +class SDLResourceManager +{ + friend class SDLResourceFile; + + public: + static SDL_RWops* get(const std::string& type, const std::string& id); + + static const std::string typeFonts(); + static const std::string typeImages(); + + protected: + static void insert(const std::string& type, const std::string& id, + const std::vector<unsigned char>& data); + + private: + SDLResourceManager() = delete; + SDLResourceManager(const SDLResourceManager& other) = delete; + SDLResourceManager(const SDLResourceManager&& other) = delete; + ~SDLResourceManager() = delete; + + static std::map<std::string, std::vector<unsigned char>>& resources(); +}; diff --git a/client/SDL/dialogs/sdl_button.cpp b/client/SDL/dialogs/sdl_button.cpp new file mode 100644 index 0000000..cfa2107 --- /dev/null +++ b/client/SDL/dialogs/sdl_button.cpp @@ -0,0 +1,71 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client Channels + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * Copyright 2023 Thincast Technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <cassert> + +#include "sdl_button.hpp" + +static const SDL_Color buttonbackgroundcolor = { 0x69, 0x66, 0x63, 0xff }; +static const SDL_Color buttonhighlightcolor = { 0xcd, 0xca, 0x35, 0x60 }; +static const SDL_Color buttonmouseovercolor = { 0x66, 0xff, 0x66, 0x60 }; +static const SDL_Color buttonfontcolor = { 0xd1, 0xcf, 0xcd, 0xff }; + +SdlButton::SdlButton(SDL_Renderer* renderer, const std::string& label, int id, const SDL_Rect& rect) + : SdlWidget(renderer, rect, false), _name(label), _id(id) +{ + assert(renderer); + + update_text(renderer, _name, buttonfontcolor, buttonbackgroundcolor); +} + +SdlButton::SdlButton(SdlButton&& other) noexcept + : SdlWidget(std::move(other)), _name(std::move(other._name)), _id(std::move(other._id)) +{ +} + +bool SdlButton::highlight(SDL_Renderer* renderer) +{ + assert(renderer); + + std::vector<SDL_Color> colors = { buttonbackgroundcolor, buttonhighlightcolor }; + if (!fill(renderer, colors)) + return false; + return update_text(renderer, _name, buttonfontcolor); +} + +bool SdlButton::mouseover(SDL_Renderer* renderer) +{ + std::vector<SDL_Color> colors = { buttonbackgroundcolor, buttonmouseovercolor }; + if (!fill(renderer, colors)) + return false; + return update_text(renderer, _name, buttonfontcolor); +} + +bool SdlButton::update(SDL_Renderer* renderer) +{ + assert(renderer); + + return update_text(renderer, _name, buttonfontcolor, buttonbackgroundcolor); +} + +int SdlButton::id() const +{ + return _id; +} diff --git a/client/SDL/dialogs/sdl_button.hpp b/client/SDL/dialogs/sdl_button.hpp new file mode 100644 index 0000000..350e7db --- /dev/null +++ b/client/SDL/dialogs/sdl_button.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include <string> + +#include "sdl_widget.hpp" + +class SdlButton : public SdlWidget +{ + public: + SdlButton(SDL_Renderer* renderer, const std::string& label, int id, const SDL_Rect& rect); + SdlButton(SdlButton&& other) noexcept; + ~SdlButton() override = default; + + bool highlight(SDL_Renderer* renderer); + bool mouseover(SDL_Renderer* renderer); + bool update(SDL_Renderer* renderer); + + [[nodiscard]] int id() const; + + private: + SdlButton(const SdlButton& other) = delete; + + private: + std::string _name; + int _id; +}; diff --git a/client/SDL/dialogs/sdl_buttons.cpp b/client/SDL/dialogs/sdl_buttons.cpp new file mode 100644 index 0000000..8190cbe --- /dev/null +++ b/client/SDL/dialogs/sdl_buttons.cpp @@ -0,0 +1,105 @@ +#include <cassert> +#include <algorithm> + +#include "sdl_buttons.hpp" + +static const Uint32 hpadding = 10; + +bool SdlButtonList::populate(SDL_Renderer* renderer, const std::vector<std::string>& labels, + const std::vector<int>& ids, Sint32 total_width, Sint32 offsetY, + Sint32 width, Sint32 height) +{ + assert(renderer); + assert(width >= 0); + assert(height >= 0); + assert(labels.size() == ids.size()); + + _list.clear(); + size_t button_width = ids.size() * (width + hpadding) + hpadding; + size_t offsetX = total_width - std::min<size_t>(total_width, button_width); + for (size_t x = 0; x < ids.size(); x++) + { + const size_t curOffsetX = offsetX + x * (static_cast<size_t>(width) + hpadding); + const SDL_Rect rect = { static_cast<int>(curOffsetX), offsetY, width, height }; + _list.emplace_back(renderer, labels[x], ids[x], rect); + } + return true; +} + +SdlButton* SdlButtonList::get_selected(const SDL_MouseButtonEvent& button) +{ + const Sint32 x = button.x; + const Sint32 y = button.y; + + return get_selected(x, y); +} + +SdlButton* SdlButtonList::get_selected(Sint32 x, Sint32 y) +{ + for (auto& btn : _list) + { + auto r = btn.rect(); + if ((x >= r.x) && (x <= r.x + r.w) && (y >= r.y) && (y <= r.y + r.h)) + return &btn; + } + return nullptr; +} + +bool SdlButtonList::set_highlight_next(bool reset) +{ + if (reset) + _highlighted = nullptr; + else + { + auto next = _highlight_index++; + _highlight_index %= _list.size(); + auto& element = _list[next]; + _highlighted = &element; + } + return true; +} + +bool SdlButtonList::set_highlight(size_t index) +{ + if (index >= _list.size()) + { + _highlighted = nullptr; + return false; + } + auto& element = _list[index]; + _highlighted = &element; + _highlight_index = ++index % _list.size(); + return true; +} + +bool SdlButtonList::set_mouseover(Sint32 x, Sint32 y) +{ + _mouseover = get_selected(x, y); + return _mouseover != nullptr; +} + +void SdlButtonList::clear() +{ + _list.clear(); + _mouseover = nullptr; + _highlighted = nullptr; + _highlight_index = 0; +} + +bool SdlButtonList::update(SDL_Renderer* renderer) +{ + assert(renderer); + + for (auto& btn : _list) + { + if (!btn.update(renderer)) + return false; + } + + if (_highlighted) + _highlighted->highlight(renderer); + + if (_mouseover) + _mouseover->mouseover(renderer); + return true; +} diff --git a/client/SDL/dialogs/sdl_buttons.hpp b/client/SDL/dialogs/sdl_buttons.hpp new file mode 100644 index 0000000..7f82903 --- /dev/null +++ b/client/SDL/dialogs/sdl_buttons.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include <vector> +#include <cstdint> + +#include "sdl_button.hpp" + +class SdlButtonList +{ + public: + SdlButtonList() = default; + virtual ~SdlButtonList() = default; + + bool populate(SDL_Renderer* renderer, const std::vector<std::string>& labels, + const std::vector<int>& ids, Sint32 total_width, Sint32 offsetY, Sint32 width, + Sint32 heigth); + + bool update(SDL_Renderer* renderer); + SdlButton* get_selected(const SDL_MouseButtonEvent& button); + SdlButton* get_selected(Sint32 x, Sint32 y); + + bool set_highlight_next(bool reset = false); + bool set_highlight(size_t index); + bool set_mouseover(Sint32 x, Sint32 y); + + void clear(); + + private: + SdlButtonList(const SdlButtonList& other) = delete; + SdlButtonList(SdlButtonList&& other) = delete; + + private: + std::vector<SdlButton> _list; + SdlButton* _highlighted = nullptr; + size_t _highlight_index = 0; + SdlButton* _mouseover = nullptr; +}; diff --git a/client/SDL/dialogs/sdl_connection_dialog.cpp b/client/SDL/dialogs/sdl_connection_dialog.cpp new file mode 100644 index 0000000..cbb6349 --- /dev/null +++ b/client/SDL/dialogs/sdl_connection_dialog.cpp @@ -0,0 +1,536 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include <cassert> +#include <thread> + +#include "sdl_connection_dialog.hpp" +#include "../sdl_utils.hpp" +#include "../sdl_freerdp.hpp" +#include "res/sdl_resource_manager.hpp" + +static const SDL_Color backgroundcolor = { 0x38, 0x36, 0x35, 0xff }; +static const SDL_Color textcolor = { 0xd1, 0xcf, 0xcd, 0xff }; +static const SDL_Color infocolor = { 0x43, 0xe0, 0x0f, 0x60 }; +static const SDL_Color warncolor = { 0xcd, 0xca, 0x35, 0x60 }; +static const SDL_Color errorcolor = { 0xf7, 0x22, 0x30, 0x60 }; + +static const Uint32 vpadding = 5; +static const Uint32 hpadding = 5; + +SDLConnectionDialog::SDLConnectionDialog(rdpContext* context) + : _context(context), _window(nullptr), _renderer(nullptr) +{ + SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO); + hide(); +} + +SDLConnectionDialog::~SDLConnectionDialog() +{ + resetTimer(); + destroyWindow(); + SDL_Quit(); +} + +bool SDLConnectionDialog::visible() const +{ + return _window && _renderer; +} + +bool SDLConnectionDialog::setTitle(const char* fmt, ...) +{ + std::lock_guard lock(_mux); + va_list ap; + va_start(ap, fmt); + _title = print(fmt, ap); + va_end(ap); + + return show(MSG_NONE); +} + +bool SDLConnectionDialog::showInfo(const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + auto rc = show(MSG_INFO, fmt, ap); + va_end(ap); + return rc; +} + +bool SDLConnectionDialog::showWarn(const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + auto rc = show(MSG_WARN, fmt, ap); + va_end(ap); + return rc; +} + +bool SDLConnectionDialog::showError(const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + auto rc = show(MSG_ERROR, fmt, ap); + va_end(ap); + return setTimer(); +} + +bool SDLConnectionDialog::show() +{ + std::lock_guard lock(_mux); + return show(_type_active); +} + +bool SDLConnectionDialog::hide() +{ + std::lock_guard lock(_mux); + return show(MSG_DISCARD); +} + +bool SDLConnectionDialog::running() const +{ + std::lock_guard lock(_mux); + return _running; +} + +bool SDLConnectionDialog::update() +{ + std::lock_guard lock(_mux); + switch (_type) + { + case MSG_INFO: + case MSG_WARN: + case MSG_ERROR: + _type_active = _type; + createWindow(); + break; + case MSG_DISCARD: + resetTimer(); + destroyWindow(); + break; + default: + if (_window) + { + SDL_SetWindowTitle(_window, _title.c_str()); + } + break; + } + _type = MSG_NONE; + return true; +} + +bool SDLConnectionDialog::setModal() +{ + if (_window) + { + auto sdl = get_context(_context); + if (sdl->windows.empty()) + return true; + + auto parent = sdl->windows.begin()->second.window(); + SDL_SetWindowModalFor(_window, parent); + SDL_RaiseWindow(_window); + } + return true; +} + +bool SDLConnectionDialog::clearWindow(SDL_Renderer* renderer) +{ + assert(renderer); + + const int drc = SDL_SetRenderDrawColor(renderer, backgroundcolor.r, backgroundcolor.g, + backgroundcolor.b, backgroundcolor.a); + if (widget_log_error(drc, "SDL_SetRenderDrawColor")) + return false; + + const int rcls = SDL_RenderClear(renderer); + return !widget_log_error(rcls, "SDL_RenderClear"); +} + +bool SDLConnectionDialog::update(SDL_Renderer* renderer) +{ + if (!renderer) + return false; + + if (!clearWindow(renderer)) + return false; + + for (auto& btn : _list) + { + if (!btn.widget.update_text(renderer, _msg, btn.fgcolor, btn.bgcolor)) + return false; + } + + if (!_buttons.update(renderer)) + return false; + + SDL_RenderPresent(renderer); + return true; +} + +bool SDLConnectionDialog::wait(bool ignoreRdpContext) +{ + while (running()) + { + if (!ignoreRdpContext) + { + if (freerdp_shall_disconnect_context(_context)) + return false; + } + std::this_thread::yield(); + } + return true; +} + +bool SDLConnectionDialog::handle(const SDL_Event& event) +{ + Uint32 windowID = 0; + if (_window) + { + windowID = SDL_GetWindowID(_window); + } + + switch (event.type) + { + case SDL_USEREVENT_RETRY_DIALOG: + return update(); + case SDL_QUIT: + resetTimer(); + destroyWindow(); + return false; + case SDL_KEYDOWN: + case SDL_KEYUP: + if (visible()) + { + auto ev = reinterpret_cast<const SDL_KeyboardEvent&>(event); + update(_renderer); + switch (event.key.keysym.sym) + { + case SDLK_RETURN: + case SDLK_RETURN2: + case SDLK_ESCAPE: + case SDLK_KP_ENTER: + if (event.type == SDL_KEYUP) + { + freerdp_abort_event(_context); + sdl_push_quit(); + } + break; + case SDLK_TAB: + _buttons.set_highlight_next(); + break; + default: + break; + } + + return windowID == ev.windowID; + } + return false; + case SDL_MOUSEMOTION: + if (visible()) + { + auto ev = reinterpret_cast<const SDL_MouseMotionEvent&>(event); + + _buttons.set_mouseover(event.button.x, event.button.y); + update(_renderer); + return windowID == ev.windowID; + } + return false; + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + if (visible()) + { + auto ev = reinterpret_cast<const SDL_MouseButtonEvent&>(event); + update(_renderer); + + auto button = _buttons.get_selected(event.button); + if (button) + { + if (event.type == SDL_MOUSEBUTTONUP) + { + freerdp_abort_event(_context); + sdl_push_quit(); + } + } + + return windowID == ev.windowID; + } + return false; + case SDL_MOUSEWHEEL: + if (visible()) + { + auto ev = reinterpret_cast<const SDL_MouseWheelEvent&>(event); + update(_renderer); + return windowID == ev.windowID; + } + return false; + case SDL_FINGERUP: + case SDL_FINGERDOWN: + if (visible()) + { + auto ev = reinterpret_cast<const SDL_TouchFingerEvent&>(event); + update(_renderer); +#if SDL_VERSION_ATLEAST(2, 0, 18) + return windowID == ev.windowID; +#else + return false; +#endif + } + return false; + case SDL_WINDOWEVENT: + { + auto ev = reinterpret_cast<const SDL_WindowEvent&>(event); + switch (ev.event) + { + case SDL_WINDOWEVENT_CLOSE: + if (windowID == ev.windowID) + { + freerdp_abort_event(_context); + sdl_push_quit(); + } + break; + default: + update(_renderer); + setModal(); + break; + } + + return windowID == ev.windowID; + } + default: + return false; + } +} + +bool SDLConnectionDialog::createWindow() +{ + destroyWindow(); + + const size_t widget_height = 50; + const size_t widget_width = 600; + const size_t total_height = 300; + + _window = SDL_CreateWindow( + _title.c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, widget_width, + total_height, SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_MOUSE_FOCUS | SDL_WINDOW_INPUT_FOCUS); + if (_window == nullptr) + { + widget_log_error(-1, "SDL_CreateWindow"); + return false; + } + setModal(); + + _renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_ACCELERATED); + if (_renderer == nullptr) + { + widget_log_error(-1, "SDL_CreateRenderer"); + return false; + } + + SDL_Color res_bgcolor; + switch (_type_active) + { + case MSG_INFO: + res_bgcolor = infocolor; + break; + case MSG_WARN: + res_bgcolor = warncolor; + break; + case MSG_ERROR: + res_bgcolor = errorcolor; + break; + case MSG_DISCARD: + default: + res_bgcolor = backgroundcolor; + break; + } + +#if defined(WITH_SDL_IMAGE_DIALOGS) + std::string res_name; + switch (_type_active) + { + case MSG_INFO: + res_name = "icon_info.svg"; + break; + case MSG_WARN: + res_name = "icon_warning.svg"; + break; + case MSG_ERROR: + res_name = "icon_error.svg"; + break; + case MSG_DISCARD: + default: + res_name = ""; + break; + } + + int height = (total_height - 3ul * vpadding) / 2ul; + SDL_Rect iconRect{ hpadding, vpadding, widget_width / 4ul - 2ul * hpadding, height }; + widget_cfg_t icon{ textcolor, + res_bgcolor, + { _renderer, iconRect, + SDLResourceManager::get(SDLResourceManager::typeImages(), res_name) } }; + _list.emplace_back(std::move(icon)); + + iconRect.y += height; + + widget_cfg_t logo{ textcolor, + backgroundcolor, + { _renderer, iconRect, + SDLResourceManager::get(SDLResourceManager::typeImages(), + "FreeRDP_Icon.svg") } }; + _list.emplace_back(std::move(logo)); + + SDL_Rect rect = { widget_width / 4ul, vpadding, widget_width * 3ul / 4ul, + total_height - 3ul * vpadding - widget_height }; +#else + SDL_Rect rect = { hpadding, vpadding, widget_width - 2ul * hpadding, + total_height - 2ul * vpadding }; +#endif + + widget_cfg_t w{ textcolor, backgroundcolor, { _renderer, rect, false } }; + w.widget.set_wrap(true, widget_width); + _list.emplace_back(std::move(w)); + rect.y += widget_height + vpadding; + + const std::vector<int> buttonids = { 1 }; + const std::vector<std::string> buttonlabels = { "cancel" }; + _buttons.populate(_renderer, buttonlabels, buttonids, widget_width, + total_height - widget_height - vpadding, + static_cast<Sint32>(widget_width / 2), static_cast<Sint32>(widget_height)); + _buttons.set_highlight(0); + + SDL_ShowWindow(_window); + SDL_RaiseWindow(_window); + + return true; +} + +void SDLConnectionDialog::destroyWindow() +{ + _buttons.clear(); + _list.clear(); + SDL_DestroyRenderer(_renderer); + SDL_DestroyWindow(_window); + _renderer = nullptr; + _window = nullptr; +} + +bool SDLConnectionDialog::show(MsgType type, const char* fmt, va_list ap) +{ + std::lock_guard lock(_mux); + _msg = print(fmt, ap); + return show(type); +} + +bool SDLConnectionDialog::show(MsgType type) +{ + _type = type; + return sdl_push_user_event(SDL_USEREVENT_RETRY_DIALOG); +} + +std::string SDLConnectionDialog::print(const char* fmt, va_list ap) +{ + int size = -1; + std::string res; + + do + { + res.resize(128); + if (size > 0) + res.resize(size); + + va_list copy; + va_copy(copy, ap); + size = vsnprintf(res.data(), res.size(), fmt, copy); + va_end(copy); + + } while ((size > 0) && (size > res.size())); + + return res; +} + +bool SDLConnectionDialog::setTimer(Uint32 timeoutMS) +{ + std::lock_guard lock(_mux); + resetTimer(); + + _timer = SDL_AddTimer(timeoutMS, &SDLConnectionDialog::timeout, this); + _running = true; + return true; +} + +void SDLConnectionDialog::resetTimer() +{ + if (_running) + SDL_RemoveTimer(_timer); + _running = false; +} + +Uint32 SDLConnectionDialog::timeout(Uint32 intervalMS, void* pvthis) +{ + auto ths = static_cast<SDLConnectionDialog*>(pvthis); + ths->hide(); + ths->_running = false; + return 0; +} + +SDLConnectionDialogHider::SDLConnectionDialogHider(freerdp* instance) + : SDLConnectionDialogHider(get(instance)) +{ +} + +SDLConnectionDialogHider::SDLConnectionDialogHider(rdpContext* context) + : SDLConnectionDialogHider(get(context)) +{ +} + +SDLConnectionDialogHider::SDLConnectionDialogHider(SDLConnectionDialog* dialog) : _dialog(dialog) +{ + if (_dialog) + { + _visible = _dialog->visible(); + if (_visible) + { + _dialog->hide(); + } + } +} + +SDLConnectionDialogHider::~SDLConnectionDialogHider() +{ + if (_dialog && _visible) + { + _dialog->show(); + } +} + +SDLConnectionDialog* SDLConnectionDialogHider::get(freerdp* instance) +{ + if (!instance) + return nullptr; + return get(instance->context); +} + +SDLConnectionDialog* SDLConnectionDialogHider::get(rdpContext* context) +{ + auto sdl = get_context(context); + if (!sdl) + return nullptr; + return sdl->connection_dialog.get(); +} diff --git a/client/SDL/dialogs/sdl_connection_dialog.hpp b/client/SDL/dialogs/sdl_connection_dialog.hpp new file mode 100644 index 0000000..f21f538 --- /dev/null +++ b/client/SDL/dialogs/sdl_connection_dialog.hpp @@ -0,0 +1,129 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <memory> +#include <mutex> +#include <string> +#include <vector> + +#include <SDL.h> + +#include <freerdp/freerdp.h> + +#include "sdl_widget.hpp" +#include "sdl_buttons.hpp" + +class SDLConnectionDialog +{ + public: + explicit SDLConnectionDialog(rdpContext* context); + SDLConnectionDialog(const SDLConnectionDialog& other) = delete; + SDLConnectionDialog(const SDLConnectionDialog&& other) = delete; + virtual ~SDLConnectionDialog(); + + bool visible() const; + + bool setTitle(const char* fmt, ...); + bool showInfo(const char* fmt, ...); + bool showWarn(const char* fmt, ...); + bool showError(const char* fmt, ...); + + bool show(); + bool hide(); + + bool running() const; + bool wait(bool ignoreRdpContextQuit = false); + + bool handle(const SDL_Event& event); + + private: + enum MsgType + { + MSG_NONE, + MSG_INFO, + MSG_WARN, + MSG_ERROR, + MSG_DISCARD + }; + + private: + bool createWindow(); + void destroyWindow(); + + bool update(); + + bool setModal(); + + bool clearWindow(SDL_Renderer* renderer); + + bool update(SDL_Renderer* renderer); + + bool show(MsgType type, const char* fmt, va_list ap); + bool show(MsgType type); + + std::string print(const char* fmt, va_list ap); + bool setTimer(Uint32 timeoutMS = 15000); + void resetTimer(); + + private: + static Uint32 timeout(Uint32 intervalMS, void* _this); + + private: + struct widget_cfg_t + { + SDL_Color fgcolor; + SDL_Color bgcolor; + SdlWidget widget; + }; + + private: + rdpContext* _context; + SDL_Window* _window; + SDL_Renderer* _renderer; + mutable std::mutex _mux; + std::string _title; + std::string _msg; + MsgType _type = MSG_NONE; + MsgType _type_active = MSG_NONE; + SDL_TimerID _timer = -1; + bool _running = false; + std::vector<widget_cfg_t> _list; + SdlButtonList _buttons; +}; + +class SDLConnectionDialogHider +{ + public: + explicit SDLConnectionDialogHider(freerdp* instance); + explicit SDLConnectionDialogHider(rdpContext* context); + + explicit SDLConnectionDialogHider(SDLConnectionDialog* dialog); + + ~SDLConnectionDialogHider(); + + private: + SDLConnectionDialog* get(freerdp* instance); + SDLConnectionDialog* get(rdpContext* context); + + private: + SDLConnectionDialog* _dialog; + bool _visible; +}; diff --git a/client/SDL/dialogs/sdl_dialogs.cpp b/client/SDL/dialogs/sdl_dialogs.cpp new file mode 100644 index 0000000..32f8457 --- /dev/null +++ b/client/SDL/dialogs/sdl_dialogs.cpp @@ -0,0 +1,621 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <vector> +#include <string> +#include <cassert> + +#include <freerdp/log.h> +#include <freerdp/utils/smartcardlogon.h> + +#include <SDL.h> + +#include "../sdl_freerdp.hpp" +#include "sdl_dialogs.hpp" +#include "sdl_input.hpp" +#include "sdl_input_widgets.hpp" +#include "sdl_select.hpp" +#include "sdl_selectlist.hpp" + +#define TAG CLIENT_TAG("SDL.dialogs") + +enum +{ + SHOW_DIALOG_ACCEPT_REJECT = 1, + SHOW_DIALOG_TIMED_ACCEPT = 2 +}; + +static const char* type_str_for_flags(UINT32 flags) +{ + const char* type = "RDP-Server"; + + if (flags & VERIFY_CERT_FLAG_GATEWAY) + type = "RDP-Gateway"; + + if (flags & VERIFY_CERT_FLAG_REDIRECT) + type = "RDP-Redirect"; + return type; +} + +static BOOL sdl_wait_for_result(rdpContext* context, Uint32 type, SDL_Event* result) +{ + const SDL_Event empty = { 0 }; + + WINPR_ASSERT(context); + WINPR_ASSERT(result); + + while (!freerdp_shall_disconnect_context(context)) + { + *result = empty; + const int rc = SDL_PeepEvents(result, 1, SDL_GETEVENT, type, type); + if (rc > 0) + return TRUE; + Sleep(1); + } + return FALSE; +} + +static int sdl_show_dialog(rdpContext* context, const char* title, const char* message, + Sint32 flags) +{ + SDL_Event event = { 0 }; + + if (!sdl_push_user_event(SDL_USEREVENT_SHOW_DIALOG, title, message, flags)) + return 0; + + if (!sdl_wait_for_result(context, SDL_USEREVENT_SHOW_RESULT, &event)) + return 0; + + return event.user.code; +} + +BOOL sdl_authenticate_ex(freerdp* instance, char** username, char** password, char** domain, + rdp_auth_reason reason) +{ + SDL_Event event = { 0 }; + BOOL res = FALSE; + + SDLConnectionDialogHider hider(instance); + + const char* target = freerdp_settings_get_server_name(instance->context->settings); + switch (reason) + { + case AUTH_NLA: + break; + + case AUTH_TLS: + case AUTH_RDP: + case AUTH_SMARTCARD_PIN: /* in this case password is pin code */ + if ((*username) && (*password)) + return TRUE; + break; + case GW_AUTH_HTTP: + case GW_AUTH_RDG: + case GW_AUTH_RPC: + target = + freerdp_settings_get_string(instance->context->settings, FreeRDP_GatewayHostname); + break; + default: + break; + } + + char* title = nullptr; + size_t titlesize = 0; + winpr_asprintf(&title, &titlesize, "Credentials required for %s", target); + + char* u = nullptr; + char* d = nullptr; + char* p = nullptr; + + assert(username); + assert(domain); + assert(password); + + u = *username; + d = *domain; + p = *password; + + if (!sdl_push_user_event(SDL_USEREVENT_AUTH_DIALOG, title, u, d, p, reason)) + goto fail; + + if (!sdl_wait_for_result(instance->context, SDL_USEREVENT_AUTH_RESULT, &event)) + goto fail; + else + { + auto arg = reinterpret_cast<SDL_UserAuthArg*>(event.padding); + + res = arg->result > 0 ? TRUE : FALSE; + + free(*username); + free(*domain); + free(*password); + *username = arg->user; + *domain = arg->domain; + *password = arg->password; + } + +fail: + free(title); + return res; +} + +BOOL sdl_choose_smartcard(freerdp* instance, SmartcardCertInfo** cert_list, DWORD count, + DWORD* choice, BOOL gateway) +{ + BOOL res = FALSE; + + WINPR_ASSERT(instance); + WINPR_ASSERT(cert_list); + WINPR_ASSERT(choice); + + SDLConnectionDialogHider hider(instance); + std::vector<std::string> strlist; + std::vector<const char*> list; + for (DWORD i = 0; i < count; i++) + { + const SmartcardCertInfo* cert = cert_list[i]; + char* reader = ConvertWCharToUtf8Alloc(cert->reader, nullptr); + char* container_name = ConvertWCharToUtf8Alloc(cert->containerName, nullptr); + + char* msg = nullptr; + size_t len = 0; + + winpr_asprintf(&msg, &len, + "%s\n\tReader: %s\n\tUser: %s@%s\n\tSubject: %s\n\tIssuer: %s\n\tUPN: %s", + container_name, reader, cert->userHint, cert->domainHint, cert->subject, + cert->issuer, cert->upn); + + strlist.emplace_back(msg); + free(msg); + free(reader); + free(container_name); + + auto& m = strlist.back(); + list.push_back(m.c_str()); + } + + SDL_Event event = { 0 }; + const char* title = "Select a logon smartcard certificate"; + if (gateway) + title = "Select a gateway logon smartcard certificate"; + if (!sdl_push_user_event(SDL_USEREVENT_SCARD_DIALOG, title, list.data(), count)) + goto fail; + + if (!sdl_wait_for_result(instance->context, SDL_USEREVENT_SCARD_RESULT, &event)) + goto fail; + + res = (event.user.code >= 0) ? TRUE : FALSE; + *choice = static_cast<DWORD>(event.user.code); + +fail: + return res; +} + +SSIZE_T sdl_retry_dialog(freerdp* instance, const char* what, size_t current, void* userarg) +{ + WINPR_ASSERT(instance); + WINPR_ASSERT(instance->context); + WINPR_ASSERT(what); + + auto sdl = get_context(instance->context); + std::lock_guard<CriticalSection> lock(sdl->critical); + WINPR_ASSERT(sdl->connection_dialog); + + sdl->connection_dialog->setTitle("Retry connection to %s", + freerdp_settings_get_server_name(instance->context->settings)); + + if ((strcmp(what, "arm-transport") != 0) && (strcmp(what, "connection") != 0)) + { + sdl->connection_dialog->showError("Unknown module %s, aborting", what); + return -1; + } + + if (current == 0) + { + if (strcmp(what, "arm-transport") == 0) + sdl->connection_dialog->showWarn("[%s] Starting your VM. It may take up to 5 minutes", + what); + } + + auto settings = instance->context->settings; + const BOOL enabled = freerdp_settings_get_bool(settings, FreeRDP_AutoReconnectionEnabled); + + if (!enabled) + { + sdl->connection_dialog->showError( + "Automatic reconnection disabled, terminating. Try to connect again later"); + return -1; + } + + const size_t max = freerdp_settings_get_uint32(settings, FreeRDP_AutoReconnectMaxRetries); + const size_t delay = freerdp_settings_get_uint32(settings, FreeRDP_TcpConnectTimeout); + if (current >= max) + { + sdl->connection_dialog->showError( + "[%s] retries exceeded. Your VM failed to start. Try again later or contact your " + "tech support for help if this keeps happening.", + what); + return -1; + } + + sdl->connection_dialog->showInfo("[%s] retry %" PRIuz "/%" PRIuz ", delaying %" PRIuz + "ms before next attempt", + what, current, max, delay); + return delay; +} + +BOOL sdl_present_gateway_message(freerdp* instance, UINT32 type, BOOL isDisplayMandatory, + BOOL isConsentMandatory, size_t length, const WCHAR* wmessage) +{ + if (!isDisplayMandatory) + return TRUE; + + char* title = nullptr; + size_t len = 0; + winpr_asprintf(&title, &len, "[gateway]"); + + Sint32 flags = 0; + if (isConsentMandatory) + flags = SHOW_DIALOG_ACCEPT_REJECT; + else if (isDisplayMandatory) + flags = SHOW_DIALOG_TIMED_ACCEPT; + char* message = ConvertWCharNToUtf8Alloc(wmessage, length, nullptr); + + SDLConnectionDialogHider hider(instance); + const int rc = sdl_show_dialog(instance->context, title, message, flags); + free(title); + free(message); + return rc > 0 ? TRUE : FALSE; +} + +int sdl_logon_error_info(freerdp* instance, UINT32 data, UINT32 type) +{ + int rc = -1; + const char* str_data = freerdp_get_logon_error_info_data(data); + const char* str_type = freerdp_get_logon_error_info_type(type); + + if (!instance || !instance->context) + return -1; + + /* ignore LOGON_MSG_SESSION_CONTINUE message */ + if (type == LOGON_MSG_SESSION_CONTINUE) + return 0; + + SDLConnectionDialogHider hider(instance); + + char* title = nullptr; + size_t tlen = 0; + winpr_asprintf(&title, &tlen, "[%s] info", + freerdp_settings_get_server_name(instance->context->settings)); + + char* message = nullptr; + size_t mlen = 0; + winpr_asprintf(&message, &mlen, "Logon Error Info %s [%s]", str_data, str_type); + + rc = sdl_show_dialog(instance->context, title, message, SHOW_DIALOG_ACCEPT_REJECT); + free(title); + free(message); + return rc; +} + +static DWORD sdl_show_ceritifcate_dialog(rdpContext* context, const char* title, + const char* message) +{ + SDLConnectionDialogHider hider(context); + if (!sdl_push_user_event(SDL_USEREVENT_CERT_DIALOG, title, message)) + return 0; + + SDL_Event event = { 0 }; + if (!sdl_wait_for_result(context, SDL_USEREVENT_CERT_RESULT, &event)) + return 0; + return static_cast<DWORD>(event.user.code); +} + +DWORD sdl_verify_changed_certificate_ex(freerdp* instance, const char* host, UINT16 port, + const char* common_name, const char* subject, + const char* issuer, const char* new_fingerprint, + const char* old_subject, const char* old_issuer, + const char* old_fingerprint, DWORD flags) +{ + const char* type = type_str_for_flags(flags); + + WINPR_ASSERT(instance); + WINPR_ASSERT(instance->context); + WINPR_ASSERT(instance->context->settings); + + SDLConnectionDialogHider hider(instance); + /* Newer versions of FreeRDP allow exposing the whole PEM by setting + * FreeRDP_CertificateCallbackPreferPEM to TRUE + */ + char* new_fp_str = nullptr; + size_t len = 0; + if (flags & VERIFY_CERT_FLAG_FP_IS_PEM) + { + winpr_asprintf(&new_fp_str, &len, + "----------- Certificate --------------\n" + "%s\n" + "--------------------------------------\n", + new_fingerprint); + } + else + winpr_asprintf(&new_fp_str, &len, "Thumbprint: %s\n", new_fingerprint); + + /* Newer versions of FreeRDP allow exposing the whole PEM by setting + * FreeRDP_CertificateCallbackPreferPEM to TRUE + */ + char* old_fp_str = nullptr; + size_t olen = 0; + if (flags & VERIFY_CERT_FLAG_FP_IS_PEM) + { + winpr_asprintf(&old_fp_str, &olen, + "----------- Certificate --------------\n" + "%s\n" + "--------------------------------------\n", + old_fingerprint); + } + else + winpr_asprintf(&old_fp_str, &olen, "Thumbprint: %s\n", old_fingerprint); + + const char* collission_str = ""; + if (flags & VERIFY_CERT_FLAG_MATCH_LEGACY_SHA1) + { + collission_str = + "A matching entry with legacy SHA1 was found in local known_hosts2 store.\n" + "If you just upgraded from a FreeRDP version before 2.0 this is expected.\n" + "The hashing algorithm has been upgraded from SHA1 to SHA256.\n" + "All manually accepted certificates must be reconfirmed!\n" + "\n"; + } + + char* title = nullptr; + size_t tlen = 0; + winpr_asprintf(&title, &tlen, "Certificate for %s:%" PRIu16 " (%s) has changed", host, port, + type); + + char* message = nullptr; + size_t mlen = 0; + winpr_asprintf(&message, &mlen, + "New Certificate details:\n" + "Common Name: %s\n" + "Subject: %s\n" + "Issuer: %s\n" + "%s\n" + "Old Certificate details:\n" + "Subject: %s\n" + "Issuer: %s\n" + "%s\n" + "%s\n" + "The above X.509 certificate does not match the certificate used for previous " + "connections.\n" + "This may indicate that the certificate has been tampered with.\n" + "Please contact the administrator of the RDP server and clarify.\n", + common_name, subject, issuer, new_fp_str, old_subject, old_issuer, old_fp_str, + collission_str); + + const DWORD rc = sdl_show_ceritifcate_dialog(instance->context, title, message); + free(title); + free(message); + free(new_fp_str); + free(old_fp_str); + + return rc; +} + +DWORD sdl_verify_certificate_ex(freerdp* instance, const char* host, UINT16 port, + const char* common_name, const char* subject, const char* issuer, + const char* fingerprint, DWORD flags) +{ + const char* type = type_str_for_flags(flags); + + /* Newer versions of FreeRDP allow exposing the whole PEM by setting + * FreeRDP_CertificateCallbackPreferPEM to TRUE + */ + char* fp_str = nullptr; + size_t len = 0; + if (flags & VERIFY_CERT_FLAG_FP_IS_PEM) + { + winpr_asprintf(&fp_str, &len, + "----------- Certificate --------------\n" + "%s\n" + "--------------------------------------\n", + fingerprint); + } + else + winpr_asprintf(&fp_str, &len, "Thumbprint: %s\n", fingerprint); + + char* title = nullptr; + size_t tlen = 0; + winpr_asprintf(&title, &tlen, "New certificate for %s:%" PRIu16 " (%s)", host, port, type); + + char* message = nullptr; + size_t mlen = 0; + winpr_asprintf( + &message, &mlen, + "Common Name: %s\n" + "Subject: %s\n" + "Issuer: %s\n" + "%s\n" + "The above X.509 certificate could not be verified, possibly because you do not have\n" + "the CA certificate in your certificate store, or the certificate has expired.\n" + "Please look at the OpenSSL documentation on how to add a private CA to the store.\n", + common_name, subject, issuer, fp_str); + + SDLConnectionDialogHider hider(instance); + const DWORD rc = sdl_show_ceritifcate_dialog(instance->context, title, message); + free(fp_str); + free(title); + free(message); + return rc; +} + +BOOL sdl_cert_dialog_show(const char* title, const char* message) +{ + int buttonid = -1; + enum + { + BUTTONID_CERT_ACCEPT_PERMANENT = 23, + BUTTONID_CERT_ACCEPT_TEMPORARY = 24, + BUTTONID_CERT_DENY = 25 + }; + const SDL_MessageBoxButtonData buttons[] = { + { 0, BUTTONID_CERT_ACCEPT_PERMANENT, "permanent" }, + { SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, BUTTONID_CERT_ACCEPT_TEMPORARY, "temporary" }, + { SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, BUTTONID_CERT_DENY, "cancel" } + }; + + const SDL_MessageBoxData data = { SDL_MESSAGEBOX_WARNING, nullptr, title, message, + ARRAYSIZE(buttons), buttons, nullptr }; + const int rc = SDL_ShowMessageBox(&data, &buttonid); + + Sint32 value = -1; + if (rc < 0) + value = 0; + else + { + switch (buttonid) + { + case BUTTONID_CERT_ACCEPT_PERMANENT: + value = 1; + break; + case BUTTONID_CERT_ACCEPT_TEMPORARY: + value = 2; + break; + default: + value = 0; + break; + } + } + + return sdl_push_user_event(SDL_USEREVENT_CERT_RESULT, value); +} + +BOOL sdl_message_dialog_show(const char* title, const char* message, Sint32 flags) +{ + int buttonid = -1; + enum + { + BUTTONID_SHOW_ACCEPT = 24, + BUTTONID_SHOW_DENY = 25 + }; + const SDL_MessageBoxButtonData buttons[] = { + { SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, BUTTONID_SHOW_ACCEPT, "accept" }, + { SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, BUTTONID_SHOW_DENY, "cancel" } + }; + + const int button_cnt = (flags & SHOW_DIALOG_ACCEPT_REJECT) ? 2 : 1; + const SDL_MessageBoxData data = { + SDL_MESSAGEBOX_WARNING, nullptr, title, message, button_cnt, buttons, nullptr + }; + const int rc = SDL_ShowMessageBox(&data, &buttonid); + + Sint32 value = -1; + if (rc < 0) + value = 0; + else + { + switch (buttonid) + { + case BUTTONID_SHOW_ACCEPT: + value = 1; + break; + default: + value = 0; + break; + } + } + + return sdl_push_user_event(SDL_USEREVENT_SHOW_RESULT, value); +} + +BOOL sdl_auth_dialog_show(const SDL_UserAuthArg* args) +{ + const std::vector<std::string> auth = { "Username: ", "Domain: ", + "Password: " }; + const std::vector<std::string> authPin = { "Device: ", "PIN: " }; + const std::vector<std::string> gw = { "GatewayUsername: ", "GatewayDomain: ", + "GatewayPassword: " }; + std::vector<std::string> prompt; + Sint32 rc = -1; + + switch (args->result) + { + case AUTH_SMARTCARD_PIN: + prompt = authPin; + break; + case AUTH_TLS: + case AUTH_RDP: + case AUTH_NLA: + prompt = auth; + break; + case GW_AUTH_HTTP: + case GW_AUTH_RDG: + case GW_AUTH_RPC: + prompt = gw; + break; + default: + break; + } + + std::vector<std::string> result; + + if (!prompt.empty()) + { + std::vector<std::string> initial{ args->user ? args->user : "Smartcard", "" }; + std::vector<Uint32> flags = { SdlInputWidget::SDL_INPUT_READONLY, + SdlInputWidget::SDL_INPUT_MASK }; + if (args->result != AUTH_SMARTCARD_PIN) + { + initial = { args->user ? args->user : "", args->domain ? args->domain : "", + args->password ? args->password : "" }; + flags = { 0, 0, SdlInputWidget::SDL_INPUT_MASK }; + } + SdlInputWidgetList ilist(args->title, prompt, initial, flags); + rc = ilist.run(result); + } + + if ((result.size() < prompt.size())) + rc = -1; + + char* user = nullptr; + char* domain = nullptr; + char* pwd = nullptr; + if (rc > 0) + { + user = _strdup(result[0].c_str()); + if (args->result == AUTH_SMARTCARD_PIN) + pwd = _strdup(result[1].c_str()); + else + { + domain = _strdup(result[1].c_str()); + pwd = _strdup(result[2].c_str()); + } + } + return sdl_push_user_event(SDL_USEREVENT_AUTH_RESULT, user, domain, pwd, rc); +} + +BOOL sdl_scard_dialog_show(const char* title, Sint32 count, const char** list) +{ + std::vector<std::string> vlist; + vlist.reserve(count); + for (Sint32 x = 0; x < count; x++) + vlist.emplace_back(list[x]); + SdlSelectList slist(title, vlist); + Sint32 value = slist.run(); + return sdl_push_user_event(SDL_USEREVENT_SCARD_RESULT, value); +} diff --git a/client/SDL/dialogs/sdl_dialogs.hpp b/client/SDL/dialogs/sdl_dialogs.hpp new file mode 100644 index 0000000..ae9bbe6 --- /dev/null +++ b/client/SDL/dialogs/sdl_dialogs.hpp @@ -0,0 +1,53 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <winpr/wtypes.h> +#include <freerdp/freerdp.h> + +#include "../sdl_types.hpp" +#include "../sdl_utils.hpp" + +BOOL sdl_authenticate_ex(freerdp* instance, char** username, char** password, char** domain, + rdp_auth_reason reason); +BOOL sdl_choose_smartcard(freerdp* instance, SmartcardCertInfo** cert_list, DWORD count, + DWORD* choice, BOOL gateway); + +SSIZE_T sdl_retry_dialog(freerdp* instance, const char* what, size_t current, void* userarg); + +DWORD sdl_verify_certificate_ex(freerdp* instance, const char* host, UINT16 port, + const char* common_name, const char* subject, const char* issuer, + const char* fingerprint, DWORD flags); + +DWORD sdl_verify_changed_certificate_ex(freerdp* instance, const char* host, UINT16 port, + const char* common_name, const char* subject, + const char* issuer, const char* new_fingerprint, + const char* old_subject, const char* old_issuer, + const char* old_fingerprint, DWORD flags); + +int sdl_logon_error_info(freerdp* instance, UINT32 data, UINT32 type); + +BOOL sdl_present_gateway_message(freerdp* instance, UINT32 type, BOOL isDisplayMandatory, + BOOL isConsentMandatory, size_t length, const WCHAR* message); + +BOOL sdl_message_dialog_show(const char* title, const char* message, Sint32 flags); +BOOL sdl_cert_dialog_show(const char* title, const char* message); +BOOL sdl_scard_dialog_show(const char* title, Sint32 count, const char** list); +BOOL sdl_auth_dialog_show(const SDL_UserAuthArg* args); diff --git a/client/SDL/dialogs/sdl_input.cpp b/client/SDL/dialogs/sdl_input.cpp new file mode 100644 index 0000000..6e7bf12 --- /dev/null +++ b/client/SDL/dialogs/sdl_input.cpp @@ -0,0 +1,177 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "sdl_input.hpp" + +#include <cassert> + +#include <string> + +#include <SDL.h> +#include <SDL_ttf.h> + +#include "sdl_widget.hpp" +#include "sdl_button.hpp" +#include "sdl_buttons.hpp" + +static const SDL_Color inputbackgroundcolor = { 0x56, 0x56, 0x56, 0xff }; +static const SDL_Color inputhighlightcolor = { 0x80, 0, 0, 0x60 }; +static const SDL_Color inputmouseovercolor = { 0, 0x80, 0, 0x60 }; +static const SDL_Color inputfontcolor = { 0xd1, 0xcf, 0xcd, 0xff }; +static const SDL_Color labelbackgroundcolor = { 0x56, 0x56, 0x56, 0xff }; +static const SDL_Color labelfontcolor = { 0xd1, 0xcf, 0xcd, 0xff }; +static const Uint32 vpadding = 5; +static const Uint32 hpadding = 10; + +SdlInputWidget::SdlInputWidget(SDL_Renderer* renderer, const std::string& label, + const std::string& initial, Uint32 flags, size_t offset, + size_t width, size_t height) + : _flags(flags), _text(initial), _text_label(label), + _label(renderer, + { 0, static_cast<int>(offset * (height + vpadding)), static_cast<int>(width), + static_cast<int>(height) }, + false), + _input(renderer, + { static_cast<int>(width + hpadding), static_cast<int>(offset * (height + vpadding)), + static_cast<int>(width), static_cast<int>(height) }, + true), + _highlight(false), _mouseover(false) +{ +} + +SdlInputWidget::SdlInputWidget(SdlInputWidget&& other) noexcept + : _flags(std::move(other._flags)), _text(std::move(other._text)), + _text_label(std::move(other._text_label)), _label(std::move(other._label)), + _input(std::move(other._input)), _highlight(other._highlight), _mouseover(other._mouseover) +{ +} + +bool SdlInputWidget::fill_label(SDL_Renderer* renderer, SDL_Color color) +{ + if (!_label.fill(renderer, color)) + return false; + return _label.update_text(renderer, _text_label, labelfontcolor); +} + +bool SdlInputWidget::update_label(SDL_Renderer* renderer) +{ + return _label.update_text(renderer, _text_label, labelfontcolor, labelbackgroundcolor); +} + +bool SdlInputWidget::set_mouseover(SDL_Renderer* renderer, bool mouseOver) +{ + if (readonly()) + return true; + _mouseover = mouseOver; + return update_input(renderer); +} + +bool SdlInputWidget::set_highlight(SDL_Renderer* renderer, bool highlight) +{ + if (readonly()) + return true; + _highlight = highlight; + return update_input(renderer); +} + +bool SdlInputWidget::update_input(SDL_Renderer* renderer) +{ + std::vector<SDL_Color> colors = { inputbackgroundcolor }; + if (_highlight) + colors.push_back(inputhighlightcolor); + if (_mouseover) + colors.push_back(inputmouseovercolor); + + if (!_input.fill(renderer, colors)) + return false; + return update_input(renderer, inputfontcolor); +} + +bool SdlInputWidget::resize_input(size_t size) +{ + _text.resize(size); + + return true; +} + +bool SdlInputWidget::set_str(SDL_Renderer* renderer, const std::string& text) +{ + if (readonly()) + return true; + _text = text; + if (!resize_input(_text.size())) + return false; + return update_input(renderer); +} + +bool SdlInputWidget::remove_str(SDL_Renderer* renderer, size_t count) +{ + if (readonly()) + return true; + + assert(renderer); + if (_text.empty()) + return true; + + if (!resize_input(_text.size() - count)) + return false; + return update_input(renderer); +} + +bool SdlInputWidget::append_str(SDL_Renderer* renderer, const std::string& str) +{ + assert(renderer); + if (readonly()) + return true; + + _text.append(str); + if (!resize_input(_text.size())) + return false; + return update_input(renderer); +} + +const SDL_Rect& SdlInputWidget::input_rect() const +{ + return _input.rect(); +} + +std::string SdlInputWidget::value() const +{ + return _text; +} + +bool SdlInputWidget::readonly() const +{ + return (_flags & SDL_INPUT_READONLY) != 0; +} + +bool SdlInputWidget::update_input(SDL_Renderer* renderer, SDL_Color fgcolor) +{ + std::string text = _text; + if (!text.empty()) + { + if (_flags & SDL_INPUT_MASK) + { + for (char& x : text) + x = '*'; + } + } + + return _input.update_text(renderer, text, fgcolor); +} diff --git a/client/SDL/dialogs/sdl_input.hpp b/client/SDL/dialogs/sdl_input.hpp new file mode 100644 index 0000000..11492d1 --- /dev/null +++ b/client/SDL/dialogs/sdl_input.hpp @@ -0,0 +1,73 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <vector> +#include <string> + +#include <SDL.h> +#include "sdl_widget.hpp" + +class SdlInputWidget +{ + public: + enum + { + SDL_INPUT_MASK = 1, + SDL_INPUT_READONLY = 2 + }; + + public: + SdlInputWidget(SDL_Renderer* renderer, const std::string& label, const std::string& initial, + Uint32 flags, size_t offset, size_t width, size_t height); + SdlInputWidget(SdlInputWidget&& other) noexcept; + + bool fill_label(SDL_Renderer* renderer, SDL_Color color); + bool update_label(SDL_Renderer* renderer); + + bool set_mouseover(SDL_Renderer* renderer, bool mouseOver); + bool set_highlight(SDL_Renderer* renderer, bool hightlight); + bool update_input(SDL_Renderer* renderer); + bool resize_input(size_t size); + + bool set_str(SDL_Renderer* renderer, const std::string& text); + bool remove_str(SDL_Renderer* renderer, size_t count); + bool append_str(SDL_Renderer* renderer, const std::string& text); + + [[nodiscard]] const SDL_Rect& input_rect() const; + [[nodiscard]] std::string value() const; + + [[nodiscard]] bool readonly() const; + + protected: + bool update_input(SDL_Renderer* renderer, SDL_Color fgclor); + + private: + SdlInputWidget(const SdlInputWidget& other) = delete; + + private: + Uint32 _flags; + std::string _text; + std::string _text_label; + SdlWidget _label; + SdlWidget _input; + bool _highlight; + bool _mouseover; +}; diff --git a/client/SDL/dialogs/sdl_input_widgets.cpp b/client/SDL/dialogs/sdl_input_widgets.cpp new file mode 100644 index 0000000..5846308 --- /dev/null +++ b/client/SDL/dialogs/sdl_input_widgets.cpp @@ -0,0 +1,299 @@ +#include <cassert> +#include <algorithm> + +#include "sdl_input_widgets.hpp" + +static const Uint32 vpadding = 5; + +SdlInputWidgetList::SdlInputWidgetList(const std::string& title, + const std::vector<std::string>& labels, + const std::vector<std::string>& initial, + const std::vector<Uint32>& flags) + : _window(nullptr), _renderer(nullptr) +{ + assert(labels.size() == initial.size()); + assert(labels.size() == flags.size()); + const std::vector<int> buttonids = { INPUT_BUTTON_ACCEPT, INPUT_BUTTON_CANCEL }; + const std::vector<std::string> buttonlabels = { "accept", "cancel" }; + + const size_t widget_width = 300; + const size_t widget_heigth = 50; + + const size_t total_width = widget_width + widget_width; + const size_t input_height = labels.size() * (widget_heigth + vpadding) + vpadding; + const size_t total_height = input_height + widget_heigth; + _window = SDL_CreateWindow( + title.c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, total_width, total_height, + SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_MOUSE_FOCUS | SDL_WINDOW_INPUT_FOCUS); + if (_window == nullptr) + { + widget_log_error(-1, "SDL_CreateWindow"); + } + else + { + + _renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_ACCELERATED); + if (_renderer == nullptr) + { + widget_log_error(-1, "SDL_CreateRenderer"); + } + else + { + for (size_t x = 0; x < labels.size(); x++) + _list.emplace_back(_renderer, labels[x], initial[x], flags[x], x, widget_width, + widget_heigth); + + _buttons.populate(_renderer, buttonlabels, buttonids, total_width, + static_cast<Sint32>(input_height), static_cast<Sint32>(widget_width), + static_cast<Sint32>(widget_heigth)); + _buttons.set_highlight(0); + } + } +} + +ssize_t SdlInputWidgetList::next(ssize_t current) +{ + size_t iteration = 0; + auto val = static_cast<size_t>(current); + + do + { + if (iteration >= _list.size()) + return -1; + + if (iteration == 0) + { + if (current < 0) + val = 0; + else + val++; + } + else + val++; + iteration++; + val %= _list.size(); + } while (!valid(static_cast<ssize_t>(val))); + return static_cast<ssize_t>(val); +} + +bool SdlInputWidgetList::valid(ssize_t current) const +{ + if (current < 0) + return false; + auto s = static_cast<size_t>(current); + if (s >= _list.size()) + return false; + return !_list[s].readonly(); +} + +SdlInputWidget* SdlInputWidgetList::get(ssize_t index) +{ + if (index < 0) + return nullptr; + auto s = static_cast<size_t>(index); + if (s >= _list.size()) + return nullptr; + return &_list[s]; +} + +SdlInputWidgetList::~SdlInputWidgetList() +{ + _list.clear(); + _buttons.clear(); + SDL_DestroyRenderer(_renderer); + SDL_DestroyWindow(_window); +} + +bool SdlInputWidgetList::update(SDL_Renderer* renderer) +{ + for (auto& btn : _list) + { + if (!btn.update_label(renderer)) + return false; + if (!btn.update_input(renderer)) + return false; + } + + return _buttons.update(renderer); +} + +ssize_t SdlInputWidgetList::get_index(const SDL_MouseButtonEvent& button) +{ + const Sint32 x = button.x; + const Sint32 y = button.y; + for (size_t i = 0; i < _list.size(); i++) + { + auto& cur = _list[i]; + auto r = cur.input_rect(); + + if ((x >= r.x) && (x <= r.x + r.w) && (y >= r.y) && (y <= r.y + r.h)) + return i; + } + return -1; +} + +int SdlInputWidgetList::run(std::vector<std::string>& result) +{ + int res = -1; + ssize_t LastActiveTextInput = -1; + ssize_t CurrentActiveTextInput = next(-1); + + if (!_window || !_renderer) + return -2; + + try + { + bool running = true; + std::vector<SDL_Keycode> pressed; + while (running) + { + if (!clear_window(_renderer)) + throw; + + if (!update(_renderer)) + throw; + + if (!_buttons.update(_renderer)) + throw; + + SDL_Event event = {}; + SDL_WaitEvent(&event); + switch (event.type) + { + case SDL_KEYUP: + { + auto it = std::remove(pressed.begin(), pressed.end(), event.key.keysym.sym); + pressed.erase(it, pressed.end()); + } + break; + case SDL_KEYDOWN: + pressed.push_back(event.key.keysym.sym); + switch (event.key.keysym.sym) + { + case SDLK_BACKSPACE: + { + auto cur = get(CurrentActiveTextInput); + if (cur) + { + if (!cur->remove_str(_renderer, 1)) + throw; + } + } + break; + case SDLK_TAB: + CurrentActiveTextInput = next(CurrentActiveTextInput); + break; + case SDLK_RETURN: + case SDLK_RETURN2: + case SDLK_KP_ENTER: + running = false; + res = INPUT_BUTTON_ACCEPT; + break; + case SDLK_ESCAPE: + running = false; + res = INPUT_BUTTON_CANCEL; + break; + case SDLK_v: + if (pressed.size() == 2) + { + if ((pressed[0] == SDLK_LCTRL) || (pressed[0] == SDLK_RCTRL)) + { + auto cur = get(CurrentActiveTextInput); + if (cur) + { + auto text = SDL_GetClipboardText(); + cur->set_str(_renderer, text); + } + } + } + break; + default: + break; + } + break; + case SDL_TEXTINPUT: + { + auto cur = get(CurrentActiveTextInput); + if (cur) + { + if (!cur->append_str(_renderer, event.text.text)) + throw; + } + } + break; + case SDL_MOUSEMOTION: + { + auto TextInputIndex = get_index(event.button); + for (auto& cur : _list) + { + if (!cur.set_mouseover(_renderer, false)) + throw; + } + if (TextInputIndex >= 0) + { + auto& cur = _list[static_cast<size_t>(TextInputIndex)]; + if (!cur.set_mouseover(_renderer, true)) + throw; + } + + _buttons.set_mouseover(event.button.x, event.button.y); + } + break; + case SDL_MOUSEBUTTONDOWN: + { + auto val = get_index(event.button); + if (valid(val)) + CurrentActiveTextInput = val; + + auto button = _buttons.get_selected(event.button); + if (button) + { + running = false; + if (button->id() == INPUT_BUTTON_CANCEL) + res = INPUT_BUTTON_CANCEL; + else + res = INPUT_BUTTON_ACCEPT; + } + } + break; + case SDL_QUIT: + res = INPUT_BUTTON_CANCEL; + running = false; + break; + default: + break; + } + + if (LastActiveTextInput != CurrentActiveTextInput) + { + if (CurrentActiveTextInput < 0) + SDL_StopTextInput(); + else + SDL_StartTextInput(); + LastActiveTextInput = CurrentActiveTextInput; + } + + for (auto& cur : _list) + { + if (!cur.set_highlight(_renderer, false)) + throw; + } + auto cur = get(CurrentActiveTextInput); + if (cur) + { + if (!cur->set_highlight(_renderer, true)) + throw; + } + + SDL_RenderPresent(_renderer); + } + + for (auto& cur : _list) + result.push_back(cur.value()); + } + catch (...) + { + } + + return res; +} diff --git a/client/SDL/dialogs/sdl_input_widgets.hpp b/client/SDL/dialogs/sdl_input_widgets.hpp new file mode 100644 index 0000000..83568ba --- /dev/null +++ b/client/SDL/dialogs/sdl_input_widgets.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include <string> +#include <vector> +#include <SDL.h> + +#include "sdl_input.hpp" +#include "sdl_buttons.hpp" + +class SdlInputWidgetList +{ + public: + SdlInputWidgetList(const std::string& title, const std::vector<std::string>& labels, + const std::vector<std::string>& initial, const std::vector<Uint32>& flags); + virtual ~SdlInputWidgetList(); + + int run(std::vector<std::string>& result); + + protected: + bool update(SDL_Renderer* renderer); + ssize_t get_index(const SDL_MouseButtonEvent& button); + + private: + SdlInputWidgetList(const SdlInputWidgetList& other) = delete; + SdlInputWidgetList(SdlInputWidgetList&& other) = delete; + + private: + enum + { + INPUT_BUTTON_ACCEPT = 1, + INPUT_BUTTON_CANCEL = -2 + }; + + private: + ssize_t next(ssize_t current); + [[nodiscard]] bool valid(ssize_t current) const; + SdlInputWidget* get(ssize_t index); + + private: + SDL_Window* _window; + SDL_Renderer* _renderer; + std::vector<SdlInputWidget> _list; + SdlButtonList _buttons; +}; diff --git a/client/SDL/dialogs/sdl_select.cpp b/client/SDL/dialogs/sdl_select.cpp new file mode 100644 index 0000000..f0e0327 --- /dev/null +++ b/client/SDL/dialogs/sdl_select.cpp @@ -0,0 +1,74 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <cassert> + +#include <string> + +#include <SDL.h> +#include <SDL_ttf.h> + +#include "sdl_select.hpp" +#include "sdl_widget.hpp" +#include "sdl_button.hpp" +#include "sdl_buttons.hpp" +#include "sdl_input_widgets.hpp" + +static const SDL_Color labelmouseovercolor = { 0, 0x80, 0, 0x60 }; +static const SDL_Color labelbackgroundcolor = { 0x69, 0x66, 0x63, 0xff }; +static const SDL_Color labelhighlightcolor = { 0xcd, 0xca, 0x35, 0x60 }; +static const SDL_Color labelfontcolor = { 0xd1, 0xcf, 0xcd, 0xff }; + +SdlSelectWidget::SdlSelectWidget(SDL_Renderer* renderer, const std::string& label, + const SDL_Rect& rect) + : SdlWidget(renderer, rect, true), _text(label), _mouseover(false), _highlight(false) +{ + update_text(renderer); +} + +SdlSelectWidget::SdlSelectWidget(SdlSelectWidget&& other) noexcept + : SdlWidget(std::move(other)), _text(std::move(other._text)), _mouseover(other._mouseover), + _highlight(other._highlight) +{ +} + +bool SdlSelectWidget::set_mouseover(SDL_Renderer* renderer, bool mouseOver) +{ + _mouseover = mouseOver; + return update_text(renderer); +} + +bool SdlSelectWidget::set_highlight(SDL_Renderer* renderer, bool highlight) +{ + _highlight = highlight; + return update_text(renderer); +} + +bool SdlSelectWidget::update_text(SDL_Renderer* renderer) +{ + assert(renderer); + std::vector<SDL_Color> colors = { labelbackgroundcolor }; + if (_highlight) + colors.push_back(labelhighlightcolor); + if (_mouseover) + colors.push_back(labelmouseovercolor); + if (!fill(renderer, colors)) + return false; + return SdlWidget::update_text(renderer, _text, labelfontcolor); +} diff --git a/client/SDL/dialogs/sdl_select.hpp b/client/SDL/dialogs/sdl_select.hpp new file mode 100644 index 0000000..af67b74 --- /dev/null +++ b/client/SDL/dialogs/sdl_select.hpp @@ -0,0 +1,46 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <string> +#include <vector> + +#include <SDL.h> +#include "sdl_widget.hpp" + +class SdlSelectWidget : public SdlWidget +{ + public: + SdlSelectWidget(SDL_Renderer* renderer, const std::string& label, const SDL_Rect& rect); + SdlSelectWidget(SdlSelectWidget&& other) noexcept; + ~SdlSelectWidget() override = default; + + bool set_mouseover(SDL_Renderer* renderer, bool mouseOver); + bool set_highlight(SDL_Renderer* renderer, bool highlight); + bool update_text(SDL_Renderer* renderer); + + private: + SdlSelectWidget(const SdlSelectWidget& other) = delete; + + private: + std::string _text; + bool _mouseover; + bool _highlight; +}; diff --git a/client/SDL/dialogs/sdl_selectlist.cpp b/client/SDL/dialogs/sdl_selectlist.cpp new file mode 100644 index 0000000..20437cc --- /dev/null +++ b/client/SDL/dialogs/sdl_selectlist.cpp @@ -0,0 +1,208 @@ +#include "sdl_selectlist.hpp" + +static const Uint32 vpadding = 5; + +SdlSelectList::SdlSelectList(const std::string& title, const std::vector<std::string>& labels) + : _window(nullptr), _renderer(nullptr) +{ + const size_t widget_height = 50; + const size_t widget_width = 600; + + const size_t total_height = labels.size() * (widget_height + vpadding) + vpadding; + _window = SDL_CreateWindow(title.c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + widget_width, total_height + widget_height, + SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_MOUSE_FOCUS | + SDL_WINDOW_INPUT_FOCUS); + if (_window == nullptr) + { + widget_log_error(-1, "SDL_CreateWindow"); + } + else + { + _renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_ACCELERATED); + if (_renderer == nullptr) + { + widget_log_error(-1, "SDL_CreateRenderer"); + } + else + { + SDL_Rect rect = { 0, 0, widget_width, widget_height }; + for (auto& label : labels) + { + _list.emplace_back(_renderer, label, rect); + rect.y += widget_height + vpadding; + } + + const std::vector<int> buttonids = { INPUT_BUTTON_ACCEPT, INPUT_BUTTON_CANCEL }; + const std::vector<std::string> buttonlabels = { "accept", "cancel" }; + _buttons.populate( + _renderer, buttonlabels, buttonids, widget_width, static_cast<Sint32>(total_height), + static_cast<Sint32>(widget_width / 2), static_cast<Sint32>(widget_height)); + _buttons.set_highlight(0); + } + } +} + +SdlSelectList::~SdlSelectList() +{ + _list.clear(); + _buttons.clear(); + SDL_DestroyRenderer(_renderer); + SDL_DestroyWindow(_window); +} + +int SdlSelectList::run() +{ + int res = -2; + ssize_t CurrentActiveTextInput = 0; + bool running = true; + + if (!_window || !_renderer) + return -2; + try + { + while (running) + { + if (!clear_window(_renderer)) + throw; + + if (!update_text()) + throw; + + if (!_buttons.update(_renderer)) + throw; + + SDL_Event event = { 0 }; + SDL_WaitEvent(&event); + switch (event.type) + { + case SDL_KEYDOWN: + switch (event.key.keysym.sym) + { + case SDLK_UP: + case SDLK_BACKSPACE: + if (CurrentActiveTextInput > 0) + CurrentActiveTextInput--; + else + CurrentActiveTextInput = _list.size() - 1; + break; + case SDLK_DOWN: + case SDLK_TAB: + if (CurrentActiveTextInput < 0) + CurrentActiveTextInput = 0; + else + CurrentActiveTextInput++; + CurrentActiveTextInput = CurrentActiveTextInput % _list.size(); + break; + case SDLK_RETURN: + case SDLK_RETURN2: + case SDLK_KP_ENTER: + running = false; + res = CurrentActiveTextInput; + break; + case SDLK_ESCAPE: + running = false; + res = INPUT_BUTTON_CANCEL; + break; + default: + break; + } + break; + case SDL_MOUSEMOTION: + { + ssize_t TextInputIndex = get_index(event.button); + reset_mouseover(); + if (TextInputIndex >= 0) + { + auto& cur = _list[TextInputIndex]; + if (!cur.set_mouseover(_renderer, true)) + throw; + } + + _buttons.set_mouseover(event.button.x, event.button.y); + } + break; + case SDL_MOUSEBUTTONDOWN: + { + auto button = _buttons.get_selected(event.button); + if (button) + { + running = false; + if (button->id() == INPUT_BUTTON_CANCEL) + res = INPUT_BUTTON_CANCEL; + else + res = CurrentActiveTextInput; + } + else + { + CurrentActiveTextInput = get_index(event.button); + } + } + break; + case SDL_QUIT: + res = INPUT_BUTTON_CANCEL; + running = false; + break; + default: + break; + } + + reset_highlight(); + if (CurrentActiveTextInput >= 0) + { + auto& cur = _list[CurrentActiveTextInput]; + if (!cur.set_highlight(_renderer, true)) + throw; + } + + SDL_RenderPresent(_renderer); + } + } + catch (...) + { + return -1; + } + return res; +} + +ssize_t SdlSelectList::get_index(const SDL_MouseButtonEvent& button) +{ + const Sint32 x = button.x; + const Sint32 y = button.y; + for (size_t i = 0; i < _list.size(); i++) + { + auto& cur = _list[i]; + auto r = cur.rect(); + + if ((x >= r.x) && (x <= r.x + r.w) && (y >= r.y) && (y <= r.y + r.h)) + return i; + } + return -1; +} + +bool SdlSelectList::update_text() +{ + for (auto& cur : _list) + { + if (!cur.update_text(_renderer)) + return false; + } + + return true; +} + +void SdlSelectList::reset_mouseover() +{ + for (auto& cur : _list) + { + cur.set_mouseover(_renderer, false); + } +} + +void SdlSelectList::reset_highlight() +{ + for (auto& cur : _list) + { + cur.set_highlight(_renderer, false); + } +} diff --git a/client/SDL/dialogs/sdl_selectlist.hpp b/client/SDL/dialogs/sdl_selectlist.hpp new file mode 100644 index 0000000..3da0e14 --- /dev/null +++ b/client/SDL/dialogs/sdl_selectlist.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include <string> +#include <vector> + +#include <SDL.h> + +#include "sdl_select.hpp" +#include "sdl_button.hpp" +#include "sdl_buttons.hpp" + +class SdlSelectList +{ + public: + SdlSelectList(const std::string& title, const std::vector<std::string>& labels); + virtual ~SdlSelectList(); + + int run(); + + private: + SdlSelectList(const SdlSelectList& other) = delete; + SdlSelectList(SdlSelectList&& other) = delete; + + private: + enum + { + INPUT_BUTTON_ACCEPT = 0, + INPUT_BUTTON_CANCEL = -2 + }; + + private: + ssize_t get_index(const SDL_MouseButtonEvent& button); + bool update_text(); + void reset_mouseover(); + void reset_highlight(); + + private: + SDL_Window* _window; + SDL_Renderer* _renderer; + std::vector<SdlSelectWidget> _list; + SdlButtonList _buttons; +}; diff --git a/client/SDL/dialogs/sdl_widget.cpp b/client/SDL/dialogs/sdl_widget.cpp new file mode 100644 index 0000000..6e11b5a --- /dev/null +++ b/client/SDL/dialogs/sdl_widget.cpp @@ -0,0 +1,280 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <cassert> +#include <cstdio> +#include <cstdlib> + +#include <SDL.h> +#include <SDL_ttf.h> + +#include "sdl_widget.hpp" +#include "../sdl_utils.hpp" + +#include "res/sdl_resource_manager.hpp" + +#include <freerdp/log.h> + +#if defined(WITH_SDL_IMAGE_DIALOGS) +#include <SDL_image.h> +#endif + +#define TAG CLIENT_TAG("SDL.widget") + +static const SDL_Color backgroundcolor = { 0x38, 0x36, 0x35, 0xff }; + +static const Uint32 hpadding = 10; + +SdlWidget::SdlWidget(SDL_Renderer* renderer, const SDL_Rect& rect, bool input) + : _rect(rect), _input(input) +{ + assert(renderer); + + auto ops = SDLResourceManager::get(SDLResourceManager::typeFonts(), + "OpenSans-VariableFont_wdth,wght.ttf"); + if (!ops) + widget_log_error(-1, "SDLResourceManager::get"); + else + { + _font = TTF_OpenFontRW(ops, 1, 64); + if (!_font) + widget_log_error(-1, "TTF_OpenFontRW"); + } +} + +#if defined(WITH_SDL_IMAGE_DIALOGS) +SdlWidget::SdlWidget(SDL_Renderer* renderer, const SDL_Rect& rect, SDL_RWops* ops) : _rect(rect) +{ + if (ops) + { + _image = IMG_LoadTexture_RW(renderer, ops, 1); + if (!_image) + widget_log_error(-1, "IMG_LoadTextureTyped_RW"); + } +} +#endif + +SdlWidget::SdlWidget(SdlWidget&& other) noexcept + : _font(std::move(other._font)), _image(other._image), _rect(std::move(other._rect)), + _input(other._input), _wrap(other._wrap), _text_width(other._text_width) +{ + other._font = nullptr; + other._image = nullptr; +} + +SDL_Texture* SdlWidget::render_text(SDL_Renderer* renderer, const std::string& text, + SDL_Color fgcolor, SDL_Rect& src, SDL_Rect& dst) +{ + auto surface = TTF_RenderUTF8_Blended(_font, text.c_str(), fgcolor); + if (!surface) + { + widget_log_error(-1, "TTF_RenderText_Blended"); + return nullptr; + } + + auto texture = SDL_CreateTextureFromSurface(renderer, surface); + SDL_FreeSurface(surface); + if (!texture) + { + widget_log_error(-1, "SDL_CreateTextureFromSurface"); + return nullptr; + } + + TTF_SizeUTF8(_font, text.c_str(), &src.w, &src.h); + + /* Do some magic: + * - Add padding before and after text + * - if text is too long only show the last elements + * - if text is too short only update used space + */ + dst = _rect; + dst.x += hpadding; + dst.w -= 2 * hpadding; + const float scale = static_cast<float>(dst.h) / static_cast<float>(src.h); + const float sws = static_cast<float>(src.w) * scale; + const float dws = static_cast<float>(dst.w) / scale; + if (static_cast<float>(dst.w) > sws) + dst.w = static_cast<int>(sws); + if (static_cast<float>(src.w) > dws) + { + src.x = src.w - static_cast<int>(dws); + src.w = static_cast<int>(dws); + } + return texture; +} + +SDL_Texture* SdlWidget::render_text_wrapped(SDL_Renderer* renderer, const std::string& text, + SDL_Color fgcolor, SDL_Rect& src, SDL_Rect& dst) +{ + Sint32 w = 0; + Sint32 h = 0; + TTF_SizeUTF8(_font, " ", &w, &h); + auto surface = TTF_RenderUTF8_Blended_Wrapped(_font, text.c_str(), fgcolor, _text_width); + if (!surface) + { + widget_log_error(-1, "TTF_RenderText_Blended"); + return nullptr; + } + + src.w = surface->w; + src.h = surface->h; + + auto texture = SDL_CreateTextureFromSurface(renderer, surface); + SDL_FreeSurface(surface); + if (!texture) + { + widget_log_error(-1, "SDL_CreateTextureFromSurface"); + return nullptr; + } + + /* Do some magic: + * - Add padding before and after text + * - if text is too long only show the last elements + * - if text is too short only update used space + */ + dst = _rect; + dst.x += hpadding; + dst.w -= 2 * hpadding; + const float scale = static_cast<float>(src.h) / static_cast<float>(src.w); + auto dh = src.h * scale; + if (dh < dst.h) + dst.h = dh; + + return texture; +} + +SdlWidget::~SdlWidget() +{ + TTF_CloseFont(_font); + if (_image) + SDL_DestroyTexture(_image); +} + +bool SdlWidget::error_ex(Uint32 res, const char* what, const char* file, size_t line, + const char* fkt) +{ + static wLog* log = nullptr; + if (!log) + log = WLog_Get(TAG); + return sdl_log_error_ex(res, log, what, file, line, fkt); +} + +static bool draw_rect(SDL_Renderer* renderer, const SDL_Rect* rect, SDL_Color color) +{ + const int drc = SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); + if (widget_log_error(drc, "SDL_SetRenderDrawColor")) + return false; + + const int rc = SDL_RenderFillRect(renderer, rect); + return !widget_log_error(rc, "SDL_RenderFillRect"); +} + +bool SdlWidget::fill(SDL_Renderer* renderer, SDL_Color color) +{ + std::vector<SDL_Color> colors = { color }; + return fill(renderer, colors); +} + +bool SdlWidget::fill(SDL_Renderer* renderer, const std::vector<SDL_Color>& colors) +{ + assert(renderer); + SDL_BlendMode mode = SDL_BLENDMODE_INVALID; + SDL_GetRenderDrawBlendMode(renderer, &mode); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); + for (auto color : colors) + { + draw_rect(renderer, &_rect, color); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_ADD); + } + SDL_SetRenderDrawBlendMode(renderer, mode); + return true; +} + +bool SdlWidget::update_text(SDL_Renderer* renderer, const std::string& text, SDL_Color fgcolor, + SDL_Color bgcolor) +{ + assert(renderer); + + if (!fill(renderer, bgcolor)) + return false; + return update_text(renderer, text, fgcolor); +} + +bool SdlWidget::wrap() const +{ + return _wrap; +} + +bool SdlWidget::set_wrap(bool wrap, size_t width) +{ + _wrap = wrap; + _text_width = width; + return _wrap; +} + +const SDL_Rect& SdlWidget::rect() const +{ + return _rect; +} + +bool SdlWidget::update_text(SDL_Renderer* renderer, const std::string& text, SDL_Color fgcolor) +{ + + if (text.empty()) + return true; + + SDL_Rect src{}; + SDL_Rect dst{}; + + SDL_Texture* texture = nullptr; + if (_image) + { + texture = _image; + dst = _rect; + auto rc = SDL_QueryTexture(_image, nullptr, nullptr, &src.w, &src.h); + if (rc < 0) + widget_log_error(rc, "SDL_QueryTexture"); + } + else if (_wrap) + texture = render_text_wrapped(renderer, text, fgcolor, src, dst); + else + texture = render_text(renderer, text, fgcolor, src, dst); + if (!texture) + return false; + + const int rc = SDL_RenderCopy(renderer, texture, &src, &dst); + if (!_image) + SDL_DestroyTexture(texture); + if (rc < 0) + return !widget_log_error(rc, "SDL_RenderCopy"); + return true; +} + +bool clear_window(SDL_Renderer* renderer) +{ + assert(renderer); + + const int drc = SDL_SetRenderDrawColor(renderer, backgroundcolor.r, backgroundcolor.g, + backgroundcolor.b, backgroundcolor.a); + if (widget_log_error(drc, "SDL_SetRenderDrawColor")) + return false; + + const int rcls = SDL_RenderClear(renderer); + return !widget_log_error(rcls, "SDL_RenderClear"); +} diff --git a/client/SDL/dialogs/sdl_widget.hpp b/client/SDL/dialogs/sdl_widget.hpp new file mode 100644 index 0000000..ebc7dbd --- /dev/null +++ b/client/SDL/dialogs/sdl_widget.hpp @@ -0,0 +1,88 @@ +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * SDL Client helper dialogs + * + * Copyright 2023 Armin Novak <armin.novak@thincast.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <string> + +#include <vector> +#include <SDL.h> +#include <SDL_ttf.h> + +#if defined(_MSC_VER) +#include <BaseTsd.h> +typedef SSIZE_T ssize_t; +#endif + +#if !defined(HAS_NOEXCEPT) +#if defined(__clang__) +#if __has_feature(cxx_noexcept) +#define HAS_NOEXCEPT +#endif +#elif defined(__GXX_EXPERIMENTAL_CXX0X__) && __GNUC__ * 10 + __GNUC_MINOR__ >= 46 || \ + defined(_MSC_FULL_VER) && _MSC_FULL_VER >= 190023026 +#define HAS_NOEXCEPT +#endif +#endif + +#ifndef HAS_NOEXCEPT +#define noexcept +#endif + +class SdlWidget +{ + public: + SdlWidget(SDL_Renderer* renderer, const SDL_Rect& rect, bool input); + SdlWidget(SDL_Renderer* renderer, const SDL_Rect& rect, SDL_RWops* ops); + SdlWidget(SdlWidget&& other) noexcept; + virtual ~SdlWidget(); + + bool fill(SDL_Renderer* renderer, SDL_Color color); + bool fill(SDL_Renderer* renderer, const std::vector<SDL_Color>& colors); + bool update_text(SDL_Renderer* renderer, const std::string& text, SDL_Color fgcolor); + bool update_text(SDL_Renderer* renderer, const std::string& text, SDL_Color fgcolor, + SDL_Color bgcolor); + + bool wrap() const; + bool set_wrap(bool wrap = true, size_t width = 0); + const SDL_Rect& rect() const; + + public: +#define widget_log_error(res, what) SdlWidget::error_ex(res, what, __FILE__, __LINE__, __func__) + static bool error_ex(Uint32 res, const char* what, const char* file, size_t line, + const char* fkt); + + private: + SdlWidget(const SdlWidget& other) = delete; + + SDL_Texture* render_text(SDL_Renderer* renderer, const std::string& text, SDL_Color fgcolor, + SDL_Rect& src, SDL_Rect& dst); + SDL_Texture* render_text_wrapped(SDL_Renderer* renderer, const std::string& text, + SDL_Color fgcolor, SDL_Rect& src, SDL_Rect& dst); + + private: + TTF_Font* _font = nullptr; + SDL_Texture* _image = nullptr; + SDL_Rect _rect; + bool _input = false; + bool _wrap = false; + size_t _text_width = 0; +}; + +bool clear_window(SDL_Renderer* renderer); diff --git a/client/SDL/dialogs/test/CMakeLists.txt b/client/SDL/dialogs/test/CMakeLists.txt new file mode 100644 index 0000000..c1003d4 --- /dev/null +++ b/client/SDL/dialogs/test/CMakeLists.txt @@ -0,0 +1,30 @@ +set(MODULE_NAME "TestSDL") +set(MODULE_PREFIX "TEST_SDL") + +set(DRIVER ${MODULE_NAME}.cpp) + +set(TEST_SRCS + TestSDLDialogs.cpp +) + +create_test_sourcelist(SRCS + ${DRIVER} + ${TEST_SRCS}) + +add_library(${MODULE_NAME} ${SRCS}) + +list(APPEND LIBS + dialogs +) + +target_link_libraries(${MODULE_NAME} ${LIBS}) + +set_target_properties(${MODULE_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${TESTING_OUTPUT_DIRECTORY}") + +foreach(test ${TESTS}) + get_filename_component(TestName ${test} NAME_WE) + add_test(${TestName} ${TESTING_OUTPUT_DIRECTORY}/${MODULE_NAME} ${TestName}) +endforeach() + +set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "FreeRDP/Client/Test") + diff --git a/client/SDL/dialogs/test/TestSDLDialogs.cpp b/client/SDL/dialogs/test/TestSDLDialogs.cpp new file mode 100644 index 0000000..558fb4c --- /dev/null +++ b/client/SDL/dialogs/test/TestSDLDialogs.cpp @@ -0,0 +1,99 @@ +#include "../sdl_selectlist.hpp" +#include "../sdl_input_widgets.hpp" + +#include <freerdp/api.h> +#include <winpr/wlog.h> + +BOOL sdl_log_error_ex(Uint32 res, wLog* log, const char* what, const char* file, size_t line, + const char* fkt) +{ + return FALSE; +} + +static bool test_input_dialog() +{ + const auto title = "sometitle"; + std::vector<std::string> labels; + std::vector<std::string> initial; + std::vector<Uint32> flags; + for (size_t x = 0; x < 12; x++) + { + labels.push_back("label" + std::to_string(x)); + initial.push_back(std::to_string(x)); + + Uint32 flag = 0; + if ((x % 2) != 0) + flag |= SdlInputWidget::SDL_INPUT_MASK; + if ((x % 3) == 0) + flag |= SdlInputWidget::SDL_INPUT_READONLY; + + flags.push_back(flag); + } + SdlInputWidgetList list{ title, labels, initial, flags }; + std::vector<std::string> result; + auto rc = list.run(result); + if (rc < 0) + { + return false; + } + if (result.size() != labels.size()) + { + return false; + } + return true; +} + +static bool test_select_dialog() +{ + const auto title = "sometitle"; + std::vector<std::string> labels; + for (size_t x = 0; x < 12; x++) + { + labels.push_back("label" + std::to_string(x)); + } + SdlSelectList list{ title, labels }; + auto rc = list.run(); + if (rc < 0) + { + return false; + } + if (static_cast<size_t>(rc) >= labels.size()) + return false; + + return true; +} + +extern "C" +{ + FREERDP_API int TestSDLDialogs(int argc, char* argv[]); +} + +int TestSDLDialogs(int argc, char* argv[]) +{ + int rc = 0; + + (void)argc; + (void)argv; + +#if 0 + SDL_Init(SDL_INIT_VIDEO); + try + { +#if 1 + if (!test_input_dialog()) + throw -1; +#endif +#if 1 + if (!test_select_dialog()) + throw -2; +#endif + } + catch (int e) + { + rc = e; + } + SDL_Quit(); + +#endif + return rc; +} |