diff --git a/.gitignore b/.gitignore index 3fe75a661f..4b5fad73cd 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ .DS_Store .vscode .vs +.cache /S2 /build /external/boost* diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a1acea949..f9feb0db59 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,7 +22,7 @@ if(DEFINED CMAKE_TOOLCHAIN_FILE) message(STATUS "Used Toolchain definition file '${CMAKE_TOOLCHAIN_FILE}'") endif() -list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/Modules" "${CMAKE_SOURCE_DIR}/external/libutil/cmake") +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/Modules" "${PROJECT_SOURCE_DIR}/external/libutil/cmake") set(checkSubmodules FALSE) # Figure out RTTR_REVISION (git hash) and RTTR_VERSION (date) @@ -242,6 +242,10 @@ include(EnableSanitizers) option(RTTR_USE_SYSTEM_LIBS "Default to using system-wide installed libs for external dependencies that have known system versions." OFF) +# Eg. Only libraries are able to run on android. The java native interface(jni) will call the SDL_main() function which calls main() in libs25client.so +# https://developer.android.com/studio/projects/add-native-code +option(RTTR_BUILD_LIB "Executables will be build as shared library." OFF) + ################################################################################ # Configure files ################################################################################ diff --git a/copyDepsToBuildDir.cmake b/copyDepsToBuildDir.cmake index f48a0e0d0c..376a63c158 100644 --- a/copyDepsToBuildDir.cmake +++ b/copyDepsToBuildDir.cmake @@ -5,7 +5,7 @@ # Needs to be configured with @ONLY and called with -DCMAKE_BUILD_TYPE=... set(CMAKE_HOST_UNIX "@CMAKE_HOST_UNIX@") -set(CMAKE_SOURCE_DIR "@CMAKE_SOURCE_DIR@") +set(PROJECT_SOURCE_DIR "@PROJECT_SOURCE_DIR@") set(RTTR_TRANSLATION_OUTPUT "@RTTR_TRANSLATION_OUTPUT@") set(RTTR_DATADIR "@RTTR_DATADIR@") set(RTTR_GAMEDIR "@RTTR_GAMEDIR@") @@ -37,9 +37,9 @@ file(COPY "${RTTR_TRANSLATION_OUTPUT}/" DESTINATION "${RTTR_OUTPUT_PATH}/RTTR/languages" FILES_MATCHING PATTERN "*.mo" ) -file(COPY "${CMAKE_SOURCE_DIR}/data/RTTR" DESTINATION "${RTTR_OUTPUT_PATH}") +file(COPY "${PROJECT_SOURCE_DIR}/data/RTTR" DESTINATION "${RTTR_OUTPUT_PATH}") -set(S2_GAME_PATHS ${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/S2) +set(S2_GAME_PATHS ${PROJECT_SOURCE_DIR} ${PROJECT_SOURCE_DIR}/S2) get_filename_component(GAME_OUTPUT_PATH "${RTTR_GAMEDIR}" ABSOLUTE BASE_DIR "${CUR_OUTPUT_DIR}") if(NOT EXISTS ${GAME_OUTPUT_PATH}/DATA) find_path(S2_DATA_DIR DATA/IO.LST PATHS ${S2_GAME_PATHS}) diff --git a/external/glad/CMakeLists.txt b/external/glad/CMakeLists.txt index d43b191eb2..5bd059dac1 100644 --- a/external/glad/CMakeLists.txt +++ b/external/glad/CMakeLists.txt @@ -9,27 +9,41 @@ include(ConstrainedOption) constrained_option(RTTR_OPENGL DEFAULT "OGL2.0Compat" DESCRIPTION "The OpenGL version to use" - VALUES OGL2.0Compat OGL2.0 OGL2.1Compat OGL2.1 OGL3.3 GLES2.0) + VALUES OGL2.0Compat OGL2.0 OGL2.1Compat OGL2.1 OGL3.3 GLES2.0 GL4ES) -string(REGEX MATCH "^(OGL|GLES)([0-9])\\.([0-9])(Compat)?$" ogl_match ${RTTR_OPENGL}) -if(NOT ogl_match) - message(FATAL_ERROR "Invalid value for RTTR_OPENGL: ${RTTR_OPENGL}.") -endif() -set(RTTR_OGL_MAJOR ${CMAKE_MATCH_2}) -set(RTTR_OGL_MINOR ${CMAKE_MATCH_3}) -if(CMAKE_MATCH_1 STREQUAL "GLES") - set(RTTR_OGL_ES 1) - set(rttr_glad_folder "OpenGLES") -else() +# gl4es OpenGL 2.1 -> GL ES 2.0 https://github.com/ptitSeb/gl4es +if(RTTR_OPENGL STREQUAL "GL4ES") + set(RTTR_OGL_MAJOR 2) + set(RTTR_OGL_MINOR 0) set(RTTR_OGL_ES 0) - set(rttr_glad_folder "OpenGL") -endif() -if(CMAKE_MATCH_4 STREQUAL "Compat") - set(RTTR_OGL_COMPAT 1) -else() set(RTTR_OGL_COMPAT 0) + set(RTTR_OGL_GL4ES 1) + + # Use default glad for gl4es + set(rttr_glad_folder "OpenGL2.0Compat") + +else() + set(RTTR_OGL_GL4ES 0) + string(REGEX MATCH "^(OGL|GLES)([0-9])\\.([0-9])(Compat)?$" ogl_match ${RTTR_OPENGL}) + if(NOT ogl_match) + message(FATAL_ERROR "Invalid value for RTTR_OPENGL: ${RTTR_OPENGL}.") + endif() + set(RTTR_OGL_MAJOR ${CMAKE_MATCH_2}) + set(RTTR_OGL_MINOR ${CMAKE_MATCH_3}) + if(CMAKE_MATCH_1 STREQUAL "GLES") + set(RTTR_OGL_ES 1) + set(rttr_glad_folder "OpenGLES") + else() + set(RTTR_OGL_ES 0) + set(rttr_glad_folder "OpenGL") + endif() + if(CMAKE_MATCH_4 STREQUAL "Compat") + set(RTTR_OGL_COMPAT 1) + else() + set(RTTR_OGL_COMPAT 0) + endif() + get_filename_component(rttr_glad_folder "${rttr_glad_folder}${RTTR_OGL_MAJOR}.${RTTR_OGL_MINOR}${CMAKE_MATCH_4}" ABSOLUTE) endif() -get_filename_component(rttr_glad_folder "${rttr_glad_folder}${RTTR_OGL_MAJOR}.${RTTR_OGL_MINOR}${CMAKE_MATCH_4}" ABSOLUTE) configure_file(openglCfg.hpp.cmake include/openglCfg.hpp @ONLY) add_library(glad STATIC ${rttr_glad_folder}/src/glad.c) diff --git a/external/glad/openglCfg.hpp.cmake b/external/glad/openglCfg.hpp.cmake index dbb3b74dcd..955367b632 100644 --- a/external/glad/openglCfg.hpp.cmake +++ b/external/glad/openglCfg.hpp.cmake @@ -8,9 +8,14 @@ // The OpenGL(ES) major and minor version to be used #define RTTR_OGL_MAJOR @RTTR_OGL_MAJOR@ #define RTTR_OGL_MINOR @RTTR_OGL_MINOR@ -// True(thy) if OpenGL ES should be used -#define RTTR_OGL_ES @RTTR_OGL_ES@ -// True(thy) if OpenGL compatibility profile should be used -#define RTTR_OGL_COMPAT @RTTR_OGL_COMPAT@ + +// 1 if OpenGL ES should be used +#cmakedefine01 RTTR_OGL_ES + +// 1 if OpenGL compatibility profile should be used +#cmakedefine01 RTTR_OGL_COMPAT + +// 1 if gl4es should be used +#cmakedefine01 RTTR_OGL_GL4ES #endif diff --git a/extras/videoDrivers/SDL2/CMakeLists.txt b/extras/videoDrivers/SDL2/CMakeLists.txt index 1c89931c66..cc599f6304 100644 --- a/extras/videoDrivers/SDL2/CMakeLists.txt +++ b/extras/videoDrivers/SDL2/CMakeLists.txt @@ -7,7 +7,26 @@ find_package(SDL2 2.0.5) if(SDL2_FOUND) add_library(videoSDL2 SHARED ${RTTR_DRIVER_INTERFACE} VideoSDL2.cpp VideoSDL2.h icon.h icon.cpp) - target_link_libraries(videoSDL2 PRIVATE videodrv s25util::common glad Boost::nowide SDL2::SDL2) + + if(RTTR_OPENGL STREQUAL "GL4ES") + if(NOT TARGET GL) + message(STATUS "Fetching gl4es from repository...") + include(FetchContent) + FetchContent_Declare( + gl4es + GIT_REPOSITORY https://github.com/ptitSeb/gl4es + GIT_TAG master + ) + FetchContent_MakeAvailable(gl4es) + target_include_directories(videoSDL2 PRIVATE + ${gl4es_SOURCE_DIR}/include + ) + endif() + # gl4es generates "GL" library + target_link_libraries(videoSDL2 PRIVATE videodrv s25util::common glad Boost::nowide SDL2::SDL2 GL) + else() + target_link_libraries(videoSDL2 PRIVATE videodrv s25util::common glad Boost::nowide SDL2::SDL2) + endif() enable_warnings(videoSDL2) if(WIN32) diff --git a/extras/videoDrivers/SDL2/VideoSDL2.cpp b/extras/videoDrivers/SDL2/VideoSDL2.cpp index 312e786454..9648841455 100644 --- a/extras/videoDrivers/SDL2/VideoSDL2.cpp +++ b/extras/videoDrivers/SDL2/VideoSDL2.cpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include #include @@ -25,13 +27,19 @@ # include // Avoid "Windows headers require the default packing option" due to SDL2 # include #endif // _WIN32 +#if RTTR_OGL_GL4ES +# include +#endif #define CHECK_SDL(call) \ - do \ - { \ - if((call) == -1) \ + ([&]() -> bool { \ + if((call) < 0) \ + { \ PrintError(SDL_GetError()); \ - } while(false) + return false; \ + } \ + return true; \ + })() namespace { template @@ -67,7 +75,11 @@ void FreeVideoInstance(IVideoDriver* driver) const char* GetDriverName() { +#if RTTR_OGL_GL4ES + return "(SDL2) OpenGL-ES gl4es via SDL2-Library"; +#else return "(SDL2) OpenGL via SDL2-Library"; +#endif } VideoSDL2::VideoSDL2(VideoDriverLoaderInterface* CallBack) : VideoDriver(CallBack), window(nullptr), context(nullptr) {} @@ -86,6 +98,8 @@ bool VideoSDL2::Initialize() { initialized = false; rttr::ScopedLeakDisabler _; + // Do not emulate mouse events using touch + SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); if(SDL_InitSubSystem(SDL_INIT_VIDEO) < 0) { PrintError(SDL_GetError()); @@ -127,12 +141,13 @@ bool VideoSDL2::CreateScreen(const std::string& title, const VideoMode& size, bo CHECK_SDL(SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, RTTR_OGL_MAJOR)); CHECK_SDL(SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, RTTR_OGL_MINOR)); SDL_GLprofile profile; - if((RTTR_OGL_ES)) + if(RTTR_OGL_ES || RTTR_OGL_GL4ES) profile = SDL_GL_CONTEXT_PROFILE_ES; - else if((RTTR_OGL_COMPAT)) + else if(RTTR_OGL_COMPAT) profile = SDL_GL_CONTEXT_PROFILE_COMPATIBILITY; else profile = SDL_GL_CONTEXT_PROFILE_CORE; + CHECK_SDL(SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, profile)); CHECK_SDL(SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8)); @@ -424,6 +439,50 @@ bool VideoSDL2::MessageLoop() } } break; + case SDL_FINGERDOWN: + { + VideoMode wnSize = GetWindowSize(); + mouse_xy.pos = getGuiScale().screenToView(Position(static_cast(ev.tfinger.x * wnSize.width), + static_cast(ev.tfinger.y * wnSize.height))); + mouse_xy.ldown = true; + mouse_xy.num_tfingers++; + CallBack->Msg_LeftDown(mouse_xy); + break; + } + case SDL_FINGERUP: + { + VideoMode wnSize = GetWindowSize(); + mouse_xy.pos = getGuiScale().screenToView(Position(static_cast(ev.tfinger.x * wnSize.width), + static_cast(ev.tfinger.y * wnSize.height))); + mouse_xy.ldown = false; + CallBack->Msg_LeftUp(mouse_xy); + mouse_xy.num_tfingers--; // Dirty way to count leftUp as touch event without extra isTouch bool + break; + } + case SDL_FINGERMOTION: + { + VideoMode wnSize = GetWindowSize(); + const auto newPos = getGuiScale().screenToView(Position( + static_cast(ev.tfinger.x * wnSize.width), static_cast(ev.tfinger.y * wnSize.height))); + + if(newPos != mouse_xy.pos) + { + mouse_xy.pos = newPos; + CallBack->Msg_MouseMove(mouse_xy); + } + break; + } + case SDL_MULTIGESTURE: + { + if(std::fabs(ev.mgesture.dDist) > 0.001) + { + if(ev.mgesture.dDist > 0) + CallBack->Msg_WheelUp(mouse_xy); + else + CallBack->Msg_WheelDown(mouse_xy); + } + break; + } } } @@ -456,7 +515,11 @@ void VideoSDL2::ListVideoModes(std::vector& video_modes) const OpenGL_Loader_Proc VideoSDL2::GetLoaderFunction() const { +#if RTTR_OGL_GL4ES + return gl4es_GetProcAddress; +#else return SDL_GL_GetProcAddress; +#endif } void VideoSDL2::SetMousePos(Position pos) @@ -491,7 +554,8 @@ void VideoSDL2::MoveWindowToCenter() SDL_Rect usableBounds; CHECK_SDL(SDL_GetDisplayUsableBounds(SDL_GetWindowDisplayIndex(window), &usableBounds)); int top, left, bottom, right; - CHECK_SDL(SDL_GetWindowBordersSize(window, &top, &left, &bottom, &right)); + if(CHECK_SDL(SDL_GetWindowBordersSize(window, &top, &left, &bottom, &right)) != 0) + top = left = bottom = right = 0; usableBounds.w -= left + right; usableBounds.h -= top + bottom; if(usableBounds.w < GetWindowSize().width || usableBounds.h < GetWindowSize().height) diff --git a/libs/driver/include/driver/MouseCoords.h b/libs/driver/include/driver/MouseCoords.h index acc174a326..2827676e38 100644 --- a/libs/driver/include/driver/MouseCoords.h +++ b/libs/driver/include/driver/MouseCoords.h @@ -14,10 +14,17 @@ struct MouseCoords MouseCoords(int x, int y) : pos(x, y) {} Position pos = Position(0, 0); - bool ldown = false; /// left button down - bool rdown = false; /// right button down - bool dbl_click = false; /// double-click (left button) + bool ldown = false; /// left button down + bool rdown = false; /// right button down + bool dbl_click = false; /// double-click (left button) + unsigned num_tfingers = 0; /// Count of fingers currently on touchscreen }; /// Maximum interval between two clicks to be considered a double-click (in milliseconds) constexpr unsigned DOUBLE_CLICK_INTERVAL = 500; + +// Max time difference in ms to trigger contextclick +constexpr unsigned TOUCH_MAX_CLICK_INTERVAL = 250; +constexpr unsigned TOUCH_DOUBLE_CLICK_INTERVAL = 175; +// Max distance between the two clicks to trigger doubleclick +constexpr unsigned TOUCH_MAX_DOUBLE_CLICK_DISTANCE = 30; diff --git a/libs/driver/include/driver/VideoDriver.h b/libs/driver/include/driver/VideoDriver.h index 94587c3eb0..c985c50c60 100644 --- a/libs/driver/include/driver/VideoDriver.h +++ b/libs/driver/include/driver/VideoDriver.h @@ -27,6 +27,9 @@ class VideoDriver : public IVideoDriver /// Funktion zum Auslesen ob die Rechte Maustaste gedrückt ist. bool GetMouseStateR() const override; + /// Function to check if at least 1 finger is on screen + bool IsTouchEvent() const override; + VideoMode GetWindowSize() const override final { return windowSize_; } Extent GetRenderSize() const override final { return scaledRenderSize_; } bool IsFullscreen() const override final { return isFullscreen_; } diff --git a/libs/driver/include/driver/VideoInterface.h b/libs/driver/include/driver/VideoInterface.h index 99f9df9b8e..06b18eedac 100644 --- a/libs/driver/include/driver/VideoInterface.h +++ b/libs/driver/include/driver/VideoInterface.h @@ -57,6 +57,8 @@ class BOOST_SYMBOL_VISIBLE IVideoDriver virtual bool GetMouseStateL() const = 0; /// Return true when right mouse button is pressed virtual bool GetMouseStateR() const = 0; + /// Return true if at least 1 finger is on screen + virtual bool IsTouchEvent() const = 0; /// Get the size of the window in screen coordinates virtual VideoMode GetWindowSize() const = 0; diff --git a/libs/driver/src/VideoDriver.cpp b/libs/driver/src/VideoDriver.cpp index 2c67adf0f8..cf9fce8913 100644 --- a/libs/driver/src/VideoDriver.cpp +++ b/libs/driver/src/VideoDriver.cpp @@ -50,6 +50,16 @@ bool VideoDriver::GetMouseStateR() const return mouse_xy.rdown; } +/** + * Function to check if at least 1 finger is on screen. + * + * @return @p true at least 1 finger, @p false when mouse used + */ +bool VideoDriver::IsTouchEvent() const +{ + return mouse_xy.num_tfingers > 0; +} + VideoMode VideoDriver::FindClosestVideoMode(const VideoMode& mode) const { std::vector avModes; diff --git a/libs/rttrConfig/src/RttrConfig.cpp b/libs/rttrConfig/src/RttrConfig.cpp index d6a6da1fb5..b42be52aa5 100644 --- a/libs/rttrConfig/src/RttrConfig.cpp +++ b/libs/rttrConfig/src/RttrConfig.cpp @@ -130,13 +130,22 @@ bool RttrConfig::Init() bfs::current_path(prefixPath_); homePath = System::getHomePath(); pathMappings.clear(); - pathMappings["BIN"] = RTTR_BINDIR; - pathMappings["EXTRA_BIN"] = RTTR_EXTRA_BINDIR; - pathMappings["DATA"] = RTTR_DATADIR; - pathMappings["GAME"] = RTTR_GAMEDIR; - pathMappings["LIB"] = RTTR_LIBDIR; - pathMappings["DRIVER"] = RTTR_DRIVERDIR; - pathMappings["RTTR"] = RTTR_DATADIR "/RTTR"; - pathMappings["USERDATA"] = RTTR_USERDATADIR; + pathMappings["BIN"] = getEnvOverride("BIN", RTTR_BINDIR); + pathMappings["EXTRA_BIN"] = getEnvOverride("EXTRA_BIN", RTTR_EXTRA_BINDIR); + pathMappings["DATA"] = getEnvOverride("DATA", RTTR_DATADIR); + pathMappings["GAME"] = getEnvOverride("GAME", RTTR_GAMEDIR); + pathMappings["LIB"] = getEnvOverride("LIB", RTTR_LIBDIR); + pathMappings["DRIVER"] = getEnvOverride("DRIVER", RTTR_DRIVERDIR); + pathMappings["RTTR"] = getEnvOverride("RTTR", RTTR_DATADIR "/RTTR"); + pathMappings["USERDATA"] = getEnvOverride("USERDATA", RTTR_USERDATADIR); return true; } + +bfs::path RttrConfig::getEnvOverride(const std::string& id, const bfs::path& defaultPath) +{ + bfs::path path = System::getPathFromEnvVar("RTTR_" + id + "_DIR"); + if(path.empty()) + return defaultPath; + LOG.write("Note: %1% path manually set to %2%\n", LogTarget::Stdout) % id % path; + return path; +} diff --git a/libs/rttrConfig/src/RttrConfig.h b/libs/rttrConfig/src/RttrConfig.h index 4b3e0b25b1..8589fde9b9 100644 --- a/libs/rttrConfig/src/RttrConfig.h +++ b/libs/rttrConfig/src/RttrConfig.h @@ -16,6 +16,9 @@ class RttrConfig : public Singleton public: bool Init(); + + /// Checks the env var for path override and returns result path + static boost::filesystem::path getEnvOverride(const std::string& id, const boost::filesystem::path& defaultPath); /// Return the prefix path for the installation static boost::filesystem::path GetPrefixPath(); /// Return the path from which RTTR was compiled diff --git a/libs/s25client/CMakeLists.txt b/libs/s25client/CMakeLists.txt index a29b0eed0f..de7317ae18 100644 --- a/libs/s25client/CMakeLists.txt +++ b/libs/s25client/CMakeLists.txt @@ -18,9 +18,20 @@ else() set(s25client_RC ) endif() -add_executable(s25client s25client.cpp commands.cpp ${s25client_RC}) -target_link_libraries(s25client PRIVATE s25Main Boost::program_options Boost::nowide rttr::vld) -add_dependencies(s25client drivers) +if(RTTR_BUILD_LIB) + add_library(s25client SHARED s25client.cpp commands.cpp ${s25client_RC}) +else() + add_executable(s25client s25client.cpp commands.cpp ${s25client_RC}) +endif() + +if(ANDROID) + find_package(SDL2 2.0.5) # SDL2 android needs a specific sdl function(SDL_main) as entry point. + target_link_libraries(s25client PRIVATE s25Main Boost::program_options Boost::nowide rttr::vld SDL2::SDL2) +else() + target_link_libraries(s25client PRIVATE s25Main Boost::program_options Boost::nowide rttr::vld) +endif() + +add_dependencies(s25client drivers translations) if(WIN32) target_include_directories(s25client PRIVATE ${rcDir}) diff --git a/libs/s25client/s25client.cpp b/libs/s25client/s25client.cpp index d6063849a2..e2a1d88f42 100644 --- a/libs/s25client/s25client.cpp +++ b/libs/s25client/s25client.cpp @@ -49,6 +49,9 @@ #ifndef _MSC_VER # include #endif +#ifdef __ANDROID__ +# include // For the sdl android entry point function (SDL_main) +#endif namespace bfs = boost::filesystem; namespace bnw = boost::nowide; diff --git a/libs/s25main/Settings.cpp b/libs/s25main/Settings.cpp index 9a2a3a9517..db1ade380e 100644 --- a/libs/s25main/Settings.cpp +++ b/libs/s25main/Settings.cpp @@ -21,6 +21,16 @@ #include "s25util/error.h" #include +namespace { +#ifdef __ANDROID__ +constexpr bool SHARED_TEXTURES_DEFAULT = false; +constexpr MapScrollMode MAP_SCROLL_MODE_DEFAULT = MapScrollMode::GrabAndDrag; +#else +constexpr bool SHARED_TEXTURES_DEFAULT = true; +constexpr MapScrollMode MAP_SCROLL_MODE_DEFAULT = MapScrollMode::ScrollOpposite; +#endif +} // namespace + const int Settings::VERSION = 13; const std::array Settings::SECTION_NAMES = { {"global", "video", "language", "driver", "sound", "lobby", "server", "proxy", "interface", "addons"}}; @@ -93,7 +103,7 @@ void Settings::LoadDefaults() } video.framerate = 0; // Special value for HW vsync video.vbo = true; - video.shared_textures = true; + video.shared_textures = SHARED_TEXTURES_DEFAULT; video.guiScale = 0; // special value indicating automatic selection // } @@ -142,7 +152,7 @@ void Settings::LoadDefaults() // interface // { interface.autosave_interval = 0; - interface.invertMouse = false; + interface.mapScrollMode = MAP_SCROLL_MODE_DEFAULT; interface.enableWindowPinning = false; interface.windowSnapDistance = 8; // } @@ -309,7 +319,16 @@ void Settings::Load() // interface // { interface.autosave_interval = iniInterface->getIntValue("autosave_interval"); - interface.invertMouse = iniInterface->getValue("invert_mouse", false); + try + { + interface.mapScrollMode = static_cast(iniInterface->getIntValue("map_scroll_mode")); + } catch(const std::runtime_error&) + { + interface.mapScrollMode = + iniInterface->getBoolValue("invert_mouse") ? MapScrollMode::ScrollSame : MapScrollMode::ScrollOpposite; + s25util::warning( + "Value 'map_scroll_mode' not found! Using 'invert_mouse' instead - please recheck your settings!"); + } interface.enableWindowPinning = iniInterface->getValue("enable_window_pinning", false); interface.windowSnapDistance = iniInterface->getValue("window_snap_distance", 8); // } @@ -475,7 +494,7 @@ void Settings::Save() // interface // { iniInterface->setValue("autosave_interval", interface.autosave_interval); - iniInterface->setValue("invert_mouse", interface.invertMouse); + iniInterface->setValue("map_scroll_mode", static_cast(interface.mapScrollMode)); iniInterface->setValue("enable_window_pinning", interface.enableWindowPinning); iniInterface->setValue("window_snap_distance", interface.windowSnapDistance); // } diff --git a/libs/s25main/Settings.h b/libs/s25main/Settings.h index 05985335ef..89fe3ba01b 100644 --- a/libs/s25main/Settings.h +++ b/libs/s25main/Settings.h @@ -31,6 +31,13 @@ struct PersistentWindowSettings bool isMinimized = false; }; +enum class MapScrollMode +{ + ScrollSame, + ScrollOpposite, // S2 Original + GrabAndDrag +}; + /// Configuration class class Settings : public Singleton { @@ -107,7 +114,7 @@ class Settings : public Singleton struct { unsigned autosave_interval; - bool invertMouse; + MapScrollMode mapScrollMode; bool enableWindowPinning; unsigned windowSnapDistance; } interface; diff --git a/libs/s25main/TerrainRenderer.cpp b/libs/s25main/TerrainRenderer.cpp index 588655194e..bb85431608 100644 --- a/libs/s25main/TerrainRenderer.cpp +++ b/libs/s25main/TerrainRenderer.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include /* Terrain rendering works like that: @@ -44,6 +45,15 @@ * providing an index and a count into the above arrays. */ +namespace { +// gl4es has some problems with GL_MODULATE so we need to change the colors without using a gl function +#if RTTR_OGL_GL4ES +constexpr float TEXTURE_COLOR_DIVISOR = 1; +#else +constexpr float TEXTURE_COLOR_DIVISOR = 2; +#endif +} // namespace + glArchivItem_Bitmap* new_clone(const glArchivItem_Bitmap& bmp) { return dynamic_cast(bmp.clone()); @@ -221,11 +231,11 @@ void TerrainRenderer::UpdateVertexColor(const MapPoint pt, const GameWorldViewer break; case Visibility::FogOfWar: // Fog of War -> abgedunkelt - GetVertex(pt).color = clr / 4.f; + GetVertex(pt).color = clr / (TEXTURE_COLOR_DIVISOR * 2); break; case Visibility::Visible: // Normal sichtbar - GetVertex(pt).color = clr / 2.f; + GetVertex(pt).color = clr / TEXTURE_COLOR_DIVISOR; break; } } @@ -791,9 +801,15 @@ void TerrainRenderer::Draw(const Position& firstPt, const Position& lastPt, cons glColorPointer(3, GL_FLOAT, 0, &gl_colors.front()); } +#if RTTR_OGL_GL4ES + // Gl4ES behaves weird with GL_COMBINE. All textures are too bright and some shadows are missing. The function might + // not be implemented in GL4ES at all. + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); +#else // Modulate2x glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); - glTexEnvf(GL_TEXTURE_ENV, GL_RGB_SCALE, 2.0f); + glTexEnvf(GL_TEXTURE_ENV, GL_RGB_SCALE, TEXTURE_COLOR_DIVISOR); +#endif // Disable alpha blending glDisable(GL_BLEND); diff --git a/libs/s25main/WindowManager.cpp b/libs/s25main/WindowManager.cpp index a832a85c6e..c1ecf13570 100644 --- a/libs/s25main/WindowManager.cpp +++ b/libs/s25main/WindowManager.cpp @@ -5,11 +5,13 @@ #include "WindowManager.h" #include "CollisionDetection.h" #include "Loader.h" +#include "Point.h" #include "RttrConfig.h" #include "Settings.h" #include "Window.h" #include "commonDefines.h" #include "desktops/Desktop.h" +#include "driver/MouseCoords.h" #include "drivers/ScreenResizeEvent.h" #include "drivers/VideoDriverWrapper.h" #include "files.h" @@ -25,6 +27,15 @@ #include "s25util/Log.h" #include "s25util/MyTime.h" #include +#include + +namespace { +template, int> = 0> +constexpr std::make_unsigned_t square(T x) +{ + return x * x; +} +} // namespace WindowManager::WindowManager() : cursor_(Cursor::Hand), disable_mouse(false), lastMousePos(Position::Invalid()), curRenderSize(0, 0), @@ -269,13 +280,25 @@ void WindowManager::Msg_LeftUp(MouseCoords mc) if(!curDesktop) return; - // Check for double-click - const auto time_now = VIDEODRIVER.GetTickCount(); - if(time_now - lastLeftClickTime < DOUBLE_CLICK_INTERVAL && mc.pos == lastLeftClickPos) + // Ggf. Doppelklick untersuche + unsigned time_now = VIDEODRIVER.GetTickCount(); + + if(VIDEODRIVER.IsTouch()) + { + if(time_now - lastLeftClickTime < TOUCH_DOUBLE_CLICK_INTERVAL) + { + // Calculate distance between two points + const Point diff = mc.pos - lastLeftClickPos; + if(square(diff.x) + square(diff.y) <= square(TOUCH_MAX_DOUBLE_CLICK_DISTANCE)) + mc.dbl_click = true; + } + + } else if(time_now - lastLeftClickTime < DOUBLE_CLICK_INTERVAL && mc.pos == lastLeftClickPos) mc.dbl_click = true; - else + + if(!mc.dbl_click) { - // Just single click, store values for next possible double click + // Save values for next potential dbl click lastLeftClickPos = mc.pos; lastLeftClickTime = time_now; } diff --git a/libs/s25main/desktops/dskGameInterface.cpp b/libs/s25main/desktops/dskGameInterface.cpp index b99076f3dc..b5dfea46c3 100644 --- a/libs/s25main/desktops/dskGameInterface.cpp +++ b/libs/s25main/desktops/dskGameInterface.cpp @@ -325,176 +325,8 @@ void dskGameInterface::Resize(const Extent& newSize) gwv.Resize(newSize); } -void dskGameInterface::Msg_ButtonClick(const unsigned ctrl_id) -{ - switch(ctrl_id) - { - case ID_btMap: WINDOWMANAGER.ToggleWindow(std::make_unique(minimap, gwv)); break; - case ID_btOptions: WINDOWMANAGER.ToggleWindow(std::make_unique(gwv, GAMECLIENT)); break; - case ID_btConstructionAid: - if(WINDOWMANAGER.IsDesktopActive()) - gwv.ToggleShowBQ(); - break; - case ID_btPost: - WINDOWMANAGER.ToggleWindow(std::make_unique(gwv, GetPostBox())); - UpdatePostIcon(GetPostBox().GetNumMsgs(), false); - break; - } -} - -void dskGameInterface::Msg_PaintBefore() -{ - Desktop::Msg_PaintBefore(); - - // Spiel ausführen - Run(); - - /// Padding of the figures - const DrawPoint figPadding(12, 12); - const DrawPoint screenSize(VIDEODRIVER.GetRenderSize()); - // Rahmen zeichnen - borders[0]->DrawFull(DrawPoint(0, 0)); // oben (mit Ecken) - borders[1]->DrawFull(DrawPoint(0, screenSize.y - figPadding.y)); // unten (mit Ecken) - borders[2]->DrawFull(DrawPoint(0, figPadding.y)); // links - borders[3]->DrawFull(DrawPoint(screenSize.x - figPadding.x, figPadding.y)); // rechts - - // The figure/statues and the button bar - glArchivItem_Bitmap& imgFigLeftTop = *LOADER.GetImageN("resource", 17); - glArchivItem_Bitmap& imgFigRightTop = *LOADER.GetImageN("resource", 18); - glArchivItem_Bitmap& imgFigLeftBot = *LOADER.GetImageN("resource", 19); - glArchivItem_Bitmap& imgFigRightBot = *LOADER.GetImageN("resource", 20); - imgFigLeftTop.DrawFull(figPadding); - imgFigRightTop.DrawFull(DrawPoint(screenSize.x - figPadding.x - imgFigRightTop.getWidth(), figPadding.y)); - imgFigLeftBot.DrawFull(DrawPoint(figPadding.x, screenSize.y - figPadding.y - imgFigLeftBot.getHeight())); - imgFigRightBot.DrawFull(screenSize - figPadding - imgFigRightBot.GetSize()); - - glArchivItem_Bitmap& imgButtonBar = *LOADER.GetImageN("resource", 29); - imgButtonBar.DrawFull( - DrawPoint((screenSize.x - imgButtonBar.getWidth()) / 2, screenSize.y - imgButtonBar.getHeight())); -} - -void dskGameInterface::Msg_PaintAfter() +bool dskGameInterface::ContextClick(const MouseCoords& mc) { - Desktop::Msg_PaintAfter(); - - const GameWorldBase& world = worldViewer.GetWorld(); - - if(SETTINGS.global.showGFInfo) - { - std::array nwf_string; - if(GAMECLIENT.IsReplayModeOn()) - { - snprintf(nwf_string.data(), nwf_string.size(), - _("(Replay-Mode) Current GF: %u (End at: %u) / GF length: %u ms / NWF length: %u gf (%u ms)"), - world.GetEvMgr().GetCurrentGF(), GAMECLIENT.GetLastReplayGF(), - GAMECLIENT.GetGFLength() / FramesInfo::milliseconds32_t(1), GAMECLIENT.GetNWFLength(), - GAMECLIENT.GetNWFLength() * GAMECLIENT.GetGFLength() / FramesInfo::milliseconds32_t(1)); - } else - snprintf(nwf_string.data(), nwf_string.size(), - _("Current GF: %u / GF length: %u ms / NWF length: %u gf (%u ms) / Ping: %u ms"), - world.GetEvMgr().GetCurrentGF(), GAMECLIENT.GetGFLength() / FramesInfo::milliseconds32_t(1), - GAMECLIENT.GetNWFLength(), - GAMECLIENT.GetNWFLength() * GAMECLIENT.GetGFLength() / FramesInfo::milliseconds32_t(1), - worldViewer.GetPlayer().ping); - NormalFont->Draw(DrawPoint(30, 1), nwf_string.data(), FontStyle{}, COLOR_YELLOW); - } - - // tournament mode? - const unsigned tournamentDuration = GAMECLIENT.GetTournamentModeDuration(); - if(tournamentDuration) - { - unsigned curGF = world.GetEvMgr().GetCurrentGF(); - std::string tournamentNotice; - if(curGF >= tournamentDuration) - tournamentNotice = _("Tournament finished"); - else - { - tournamentNotice = - helpers::format("Tournament mode: %1% remaining", GAMECLIENT.FormatGFTime(tournamentDuration - curGF)); - } - NormalFont->Draw(DrawPoint(VIDEODRIVER.GetRenderSize().x - 30, 1), tournamentNotice, FontStyle::AlignH::RIGHT, - COLOR_YELLOW); - } - - // Replaydateianzeige in der linken unteren Ecke - if(GAMECLIENT.IsReplayModeOn()) - { - NormalFont->Draw(DrawPoint(0, VIDEODRIVER.GetRenderSize().y), GAMECLIENT.GetReplayFilename().string(), - FontStyle::BOTTOM, COLOR_YELLOW); - } else - { - // Laggende Spieler anzeigen in Form von Schnecken - DrawPoint snailPos(VIDEODRIVER.GetRenderSize().x - 70, 35); - for(const NWFPlayerInfo& player : nwfInfo_->getPlayerInfos()) - { - if(player.isLagging) - { - LOADER.GetPlayerImage("rttr", 0)->DrawFull(Rect(snailPos, 30, 30), COLOR_WHITE, - game_->world_.GetPlayer(player.id).color); - snailPos.x -= 40; - } - } - } - - // Show icons in the upper right corner of the game interface - DrawPoint iconPos(VIDEODRIVER.GetRenderSize().x - 56, 32); - - // Draw cheating indicator icon (WINTER) - if(cheats_.isCheatModeOn()) - { - glArchivItem_Bitmap* cheatingImg = LOADER.GetImageN("io", 75); - cheatingImg->DrawFull(iconPos); - iconPos -= DrawPoint(cheatingImg->getWidth() + 6, 0); - } - - // Draw speed indicator icon - const int speedStep = static_cast(REFERENCE_SPEED / 10ms) - static_cast(GAMECLIENT.GetGFLength() / 10ms); - - if(speedStep != 0) - { - glArchivItem_Bitmap* runnerImg = LOADER.GetImageN("io", 164); - - runnerImg->DrawFull(iconPos); - - if(speedStep != 1) - { - std::string multiplier = helpers::toString(std::abs(speedStep)); - NormalFont->Draw(iconPos - runnerImg->GetOrigin() + DrawPoint(19, 6), multiplier, FontStyle::LEFT, - speedStep > 0 ? COLOR_YELLOW : COLOR_RED); - } - iconPos -= DrawPoint(runnerImg->getWidth() + 4, 0); - } - - // Draw zoom level indicator icon - if(gwv.GetCurrentTargetZoomFactor() != 1.f) //-V550 - { - glArchivItem_Bitmap* magnifierImg = LOADER.GetImageN("io", 36); - - magnifierImg->DrawFull(iconPos); - - std::string zoom_percent = helpers::toString((int)(gwv.GetCurrentTargetZoomFactor() * 100)) + "%"; - NormalFont->Draw(iconPos - magnifierImg->GetOrigin() + DrawPoint(9, 7), zoom_percent, FontStyle::CENTER, - COLOR_YELLOW); - iconPos -= DrawPoint(magnifierImg->getWidth() + 4, 0); - } -} - -bool dskGameInterface::Msg_LeftDown(const MouseCoords& mc) -{ - DrawPoint btOrig(VIDEODRIVER.GetRenderSize().x / 2 - LOADER.GetImageN("resource", 29)->getWidth() / 2 + 44, - VIDEODRIVER.GetRenderSize().y - LOADER.GetImageN("resource", 29)->getHeight() + 4); - Extent btSize = Extent(37, 32) * 4u; - if(IsPointInRect(mc.pos, Rect(btOrig, btSize))) - return false; - - // Start scrolling also on Ctrl + left click - if(VIDEODRIVER.GetModKeyState().ctrl) - { - Msg_RightDown(mc); - return true; - } else if(isScrolling) - StopScrolling(); - // Unterscheiden je nachdem Straäcnbaumodus an oder aus ist if(road.mode != RoadBuildMode::Disabled) { @@ -707,37 +539,242 @@ bool dskGameInterface::Msg_LeftDown(const MouseCoords& mc) return true; } -bool dskGameInterface::Msg_LeftUp(const MouseCoords&) +void dskGameInterface::Msg_ButtonClick(const unsigned ctrl_id) +{ + switch(ctrl_id) + { + case ID_btMap: WINDOWMANAGER.ToggleWindow(std::make_unique(minimap, gwv)); break; + case ID_btOptions: WINDOWMANAGER.ToggleWindow(std::make_unique(gwv, GAMECLIENT)); break; + case ID_btConstructionAid: + if(WINDOWMANAGER.IsDesktopActive()) + gwv.ToggleShowBQ(); + break; + case ID_btPost: + WINDOWMANAGER.ToggleWindow(std::make_unique(gwv, GetPostBox())); + UpdatePostIcon(GetPostBox().GetNumMsgs(), false); + break; + } +} + +void dskGameInterface::Msg_PaintBefore() +{ + Desktop::Msg_PaintBefore(); + + // Spiel ausführen + Run(); + + /// Padding of the figures + const DrawPoint figPadding(12, 12); + const DrawPoint screenSize(VIDEODRIVER.GetRenderSize()); + // Rahmen zeichnen + borders[0]->DrawFull(DrawPoint(0, 0)); // oben (mit Ecken) + borders[1]->DrawFull(DrawPoint(0, screenSize.y - figPadding.y)); // unten (mit Ecken) + borders[2]->DrawFull(DrawPoint(0, figPadding.y)); // links + borders[3]->DrawFull(DrawPoint(screenSize.x - figPadding.x, figPadding.y)); // rechts + + // The figure/statues and the button bar + glArchivItem_Bitmap& imgFigLeftTop = *LOADER.GetImageN("resource", 17); + glArchivItem_Bitmap& imgFigRightTop = *LOADER.GetImageN("resource", 18); + glArchivItem_Bitmap& imgFigLeftBot = *LOADER.GetImageN("resource", 19); + glArchivItem_Bitmap& imgFigRightBot = *LOADER.GetImageN("resource", 20); + imgFigLeftTop.DrawFull(figPadding); + imgFigRightTop.DrawFull(DrawPoint(screenSize.x - figPadding.x - imgFigRightTop.getWidth(), figPadding.y)); + imgFigLeftBot.DrawFull(DrawPoint(figPadding.x, screenSize.y - figPadding.y - imgFigLeftBot.getHeight())); + imgFigRightBot.DrawFull(screenSize - figPadding - imgFigRightBot.GetSize()); + + glArchivItem_Bitmap& imgButtonBar = *LOADER.GetImageN("resource", 29); + imgButtonBar.DrawFull( + DrawPoint((screenSize.x - imgButtonBar.getWidth()) / 2, screenSize.y - imgButtonBar.getHeight())); +} + +void dskGameInterface::Msg_PaintAfter() +{ + Desktop::Msg_PaintAfter(); + + const GameWorldBase& world = worldViewer.GetWorld(); + + if(SETTINGS.global.showGFInfo) + { + std::array nwf_string; + if(GAMECLIENT.IsReplayModeOn()) + { + snprintf(nwf_string.data(), nwf_string.size(), + _("(Replay-Mode) Current GF: %u (End at: %u) / GF length: %u ms / NWF length: %u gf (%u ms)"), + world.GetEvMgr().GetCurrentGF(), GAMECLIENT.GetLastReplayGF(), + GAMECLIENT.GetGFLength() / FramesInfo::milliseconds32_t(1), GAMECLIENT.GetNWFLength(), + GAMECLIENT.GetNWFLength() * GAMECLIENT.GetGFLength() / FramesInfo::milliseconds32_t(1)); + } else + snprintf(nwf_string.data(), nwf_string.size(), + _("Current GF: %u / GF length: %u ms / NWF length: %u gf (%u ms) / Ping: %u ms"), + world.GetEvMgr().GetCurrentGF(), GAMECLIENT.GetGFLength() / FramesInfo::milliseconds32_t(1), + GAMECLIENT.GetNWFLength(), + GAMECLIENT.GetNWFLength() * GAMECLIENT.GetGFLength() / FramesInfo::milliseconds32_t(1), + worldViewer.GetPlayer().ping); + NormalFont->Draw(DrawPoint(30, 1), nwf_string.data(), FontStyle{}, COLOR_YELLOW); + } + + // tournament mode? + const unsigned tournamentDuration = GAMECLIENT.GetTournamentModeDuration(); + if(tournamentDuration) + { + unsigned curGF = world.GetEvMgr().GetCurrentGF(); + std::string tournamentNotice; + if(curGF >= tournamentDuration) + tournamentNotice = _("Tournament finished"); + else + { + tournamentNotice = + helpers::format("Tournament mode: %1% remaining", GAMECLIENT.FormatGFTime(tournamentDuration - curGF)); + } + NormalFont->Draw(DrawPoint(VIDEODRIVER.GetRenderSize().x - 30, 1), tournamentNotice, FontStyle::AlignH::RIGHT, + COLOR_YELLOW); + } + + // Replaydateianzeige in der linken unteren Ecke + if(GAMECLIENT.IsReplayModeOn()) + { + NormalFont->Draw(DrawPoint(0, VIDEODRIVER.GetRenderSize().y), GAMECLIENT.GetReplayFilename().string(), + FontStyle::BOTTOM, COLOR_YELLOW); + } else + { + // Laggende Spieler anzeigen in Form von Schnecken + DrawPoint snailPos(VIDEODRIVER.GetRenderSize().x - 70, 35); + for(const NWFPlayerInfo& player : nwfInfo_->getPlayerInfos()) + { + if(player.isLagging) + { + LOADER.GetPlayerImage("rttr", 0)->DrawFull(Rect(snailPos, 30, 30), COLOR_WHITE, + game_->world_.GetPlayer(player.id).color); + snailPos.x -= 40; + } + } + } + + // Show icons in the upper right corner of the game interface + DrawPoint iconPos(VIDEODRIVER.GetRenderSize().x - 56, 32); + + // Draw cheating indicator icon (WINTER) + if(cheats_.isCheatModeOn()) + { + glArchivItem_Bitmap* cheatingImg = LOADER.GetImageN("io", 75); + cheatingImg->DrawFull(iconPos); + iconPos -= DrawPoint(cheatingImg->getWidth() + 6, 0); + } + + // Draw speed indicator icon + const int speedStep = static_cast(REFERENCE_SPEED / 10ms) - static_cast(GAMECLIENT.GetGFLength() / 10ms); + + if(speedStep != 0) + { + glArchivItem_Bitmap* runnerImg = LOADER.GetImageN("io", 164); + + runnerImg->DrawFull(iconPos); + + if(speedStep != 1) + { + std::string multiplier = helpers::toString(std::abs(speedStep)); + NormalFont->Draw(iconPos - runnerImg->GetOrigin() + DrawPoint(19, 6), multiplier, FontStyle::LEFT, + speedStep > 0 ? COLOR_YELLOW : COLOR_RED); + } + iconPos -= DrawPoint(runnerImg->getWidth() + 4, 0); + } + + // Draw zoom level indicator icon + if(gwv.GetCurrentTargetZoomFactor() != 1.f) //-V550 + { + glArchivItem_Bitmap* magnifierImg = LOADER.GetImageN("io", 36); + + magnifierImg->DrawFull(iconPos); + + std::string zoom_percent = helpers::toString((int)(gwv.GetCurrentTargetZoomFactor() * 100)) + "%"; + NormalFont->Draw(iconPos - magnifierImg->GetOrigin() + DrawPoint(9, 7), zoom_percent, FontStyle::CENTER, + COLOR_YELLOW); + iconPos -= DrawPoint(magnifierImg->getWidth() + 4, 0); + } +} + +bool dskGameInterface::Msg_LeftDown(const MouseCoords& mc) +{ + DrawPoint btOrig(VIDEODRIVER.GetRenderSize().x / 2 - LOADER.GetImageN("resource", 29)->getWidth() / 2 + 44, + VIDEODRIVER.GetRenderSize().y - LOADER.GetImageN("resource", 29)->getHeight() + 4); + Extent btSize = Extent(37, 32) * 4u; + if(IsPointInRect(mc.pos, Rect(btOrig, btSize))) + return false; + + if(!VIDEODRIVER.IsTouch()) + { + // Start scrolling also on Ctrl + left click + if(VIDEODRIVER.GetModKeyState().ctrl) + { + Msg_RightDown(mc); + return true; + } else if(isScrolling) + StopScrolling(); + + return ContextClick(mc); + + } else if(mc.num_tfingers < 2) + touchDuration = VIDEODRIVER.GetTickCount(); + else if(isScrolling) // 2 fingers down -> zoom mode. Do not click or scroll map + StopScrolling(); + + return true; +} + +bool dskGameInterface::Msg_LeftUp(const MouseCoords& mc) { if(isScrolling) { StopScrolling(); return true; } + + // num_tfingers is reduced after this function to check if it's still a touch event + // Was touch duration short enough to trigger conext click? + if(mc.num_tfingers == 1 && (VIDEODRIVER.GetTickCount() - touchDuration) < TOUCH_MAX_CLICK_INTERVAL) + return ContextClick(mc); + return false; } bool dskGameInterface::Msg_MouseMove(const MouseCoords& mc) { if(!isScrolling) - return false; + { + if(mc.num_tfingers == 1) + Msg_RightDown(mc); + else + return false; + } + + if(SETTINGS.interface.mapScrollMode == MapScrollMode::GrabAndDrag) + { + const Position mapPos = gwv.ViewPosToMap(mc.pos); + gwv.MoveBy(-(mapPos - startScrollPt)); + startScrollPt = mapPos; + } else + { + int acceleration = SETTINGS.global.smartCursor ? 2 : 3; - int acceleration = SETTINGS.global.smartCursor ? 2 : 3; + if(SETTINGS.interface.mapScrollMode == MapScrollMode::ScrollSame) + acceleration = -acceleration; - if(SETTINGS.interface.invertMouse) - acceleration = -acceleration; + gwv.MoveBy((mc.pos - startScrollPt) * acceleration); + VIDEODRIVER.SetMousePos(startScrollPt); - gwv.MoveBy((mc.pos - startScrollPt) * acceleration); - VIDEODRIVER.SetMousePos(startScrollPt); + if(!SETTINGS.global.smartCursor) + startScrollPt = mc.pos; + } - if(!SETTINGS.global.smartCursor) - startScrollPt = mc.pos; return true; } bool dskGameInterface::Msg_RightDown(const MouseCoords& mc) { - StartScrolling(mc.pos); + if(SETTINGS.interface.mapScrollMode == MapScrollMode::GrabAndDrag) + StartScrolling(gwv.ViewPosToMap(mc.pos)); + else + StartScrolling(mc.pos); return true; } diff --git a/libs/s25main/desktops/dskGameInterface.h b/libs/s25main/desktops/dskGameInterface.h index 0efd7e8bd0..483f6539fb 100644 --- a/libs/s25main/desktops/dskGameInterface.h +++ b/libs/s25main/desktops/dskGameInterface.h @@ -117,6 +117,9 @@ class dskGameInterface : /// Updatet das Post-Icon mit der Nachrichtenanzahl und der Taube void UpdatePostIcon(unsigned postmessages_count, bool showPigeon); + /// Executed during left click. Checks click pos for buildings/roads + bool ContextClick(const MouseCoords& mc); + void Msg_ButtonClick(unsigned ctrl_id) override; void Msg_PaintBefore() override; void Msg_PaintAfter() override; @@ -163,6 +166,8 @@ class dskGameInterface : /// Minimap-Instanz IngameMinimap minimap; + // How long is finger on screen (contextclick or scrolling?) + unsigned int touchDuration; bool isScrolling; Position startScrollPt; Subscription evBld; diff --git a/libs/s25main/desktops/dskOptions.cpp b/libs/s25main/desktops/dskOptions.cpp index 1b1fb403e2..1048729aa5 100644 --- a/libs/s25main/desktops/dskOptions.cpp +++ b/libs/s25main/desktops/dskOptions.cpp @@ -4,7 +4,6 @@ #include "dskOptions.h" #include "GlobalGameSettings.h" -#include "GlobalVars.h" #include "Loader.h" #include "MusicPlayer.h" #include "Settings.h" @@ -69,8 +68,8 @@ enum ID_grpDebugData, ID_txtUPNP, ID_grpUPNP, - ID_txtInvertScroll, - ID_grpInvertScroll, + ID_txtMapScrollMode, + ID_cbMapScrollMode, ID_txtSmartCursor, ID_grpSmartCursor, ID_txtWindowPinning, @@ -276,13 +275,14 @@ dskOptions::dskOptions() : Desktop(LOADER.GetImageN("setup013", 0)) curPos.y += rowHeight; curPos.y += sectionSpacingCommon; - groupCommon->AddText(ID_txtInvertScroll, curPos, _("Invert Mouse Pan:"), COLOR_YELLOW, FontStyle{}, NormalFont); - ctrlOptionGroup* invertScroll = groupCommon->AddOptionGroup(ID_grpInvertScroll, GroupSelectType::Check); - invertScroll->AddTextButton(ID_btOn, curPos + ctrlOffset, ctrlSize, TextureColor::Grey, _("On"), NormalFont, - _("Map moves in the opposite direction the mouse is moved when scrolling/panning.")); - invertScroll->AddTextButton(ID_btOff, curPos + ctrlOffset2, ctrlSize, TextureColor::Grey, _("Off"), NormalFont, - _("Map moves in the same direction the mouse is moved when scrolling/panning.")); - invertScroll->SetSelection(SETTINGS.interface.invertMouse); + groupCommon->AddText(ID_txtMapScrollMode, curPos, _("Map scroll mode:"), COLOR_YELLOW, FontStyle{}, NormalFont); + combo = groupCommon->AddComboBox(ID_cbMapScrollMode, curPos + ctrlOffset, ctrlSizeLarge, TextureColor::Grey, + NormalFont, 100); + combo->AddString(_("Scroll same (Map moves in the same direction the mouse is moved when scrolling/panning.)")); + combo->AddString( + _("Scroll opposite (Map moves in the opposite direction the mouse is moved when scrolling/panning.)")); + combo->AddString(_("Grab and drag (Map moves with your cursor when scrolling/panning.)")); + combo->SetSelection(static_cast(SETTINGS.interface.mapScrollMode)); curPos.y += rowHeight; groupCommon->AddText(ID_txtSmartCursor, curPos, _("Smart Cursor"), COLOR_YELLOW, FontStyle{}, NormalFont); @@ -562,6 +562,7 @@ void dskOptions::Msg_Group_ComboSelectItem(const unsigned group_id, const unsign ->GetCtrl(1) ->SetEnabled(true); break; + case ID_cbMapScrollMode: SETTINGS.interface.mapScrollMode = static_cast(selection); break; case ID_cbResolution: SETTINGS.video.fullscreenSize = video_modes[selection]; break; case ID_cbFramerate: if(VIDEODRIVER.HasVSync()) @@ -608,7 +609,6 @@ void dskOptions::Msg_Group_OptionGroupChange(const unsigned /*group_id*/, const SETTINGS.global.submit_debug_data = selection; break; case ID_grpUPNP: SETTINGS.global.use_upnp = enabled; break; - case ID_grpInvertScroll: SETTINGS.interface.invertMouse = enabled; break; case ID_grpSmartCursor: SETTINGS.global.smartCursor = enabled; VIDEODRIVER.SetMouseWarping(enabled); diff --git a/libs/s25main/drivers/VideoDriverWrapper.cpp b/libs/s25main/drivers/VideoDriverWrapper.cpp index edf4233409..33659d737a 100644 --- a/libs/s25main/drivers/VideoDriverWrapper.cpp +++ b/libs/s25main/drivers/VideoDriverWrapper.cpp @@ -440,6 +440,14 @@ bool VideoDriverWrapper::IsRightDown() return videodriver->GetMouseStateR(); } +bool VideoDriverWrapper::IsTouch() +{ + if(!videodriver) + return false; + + return videodriver->IsTouchEvent(); +} + void VideoDriverWrapper::SetMousePos(const Position& newPos) { if(!videodriver || !enableMouseWarping) diff --git a/libs/s25main/drivers/VideoDriverWrapper.h b/libs/s25main/drivers/VideoDriverWrapper.h index 8b21b9d596..0124e02c32 100644 --- a/libs/s25main/drivers/VideoDriverWrapper.h +++ b/libs/s25main/drivers/VideoDriverWrapper.h @@ -85,6 +85,7 @@ class VideoDriverWrapper : public SingletonMoveBy((mc.pos - scrollOrigin) * acceleration); diff --git a/libs/s25main/ingameWindows/iwSettings.cpp b/libs/s25main/ingameWindows/iwSettings.cpp index e7f9502062..cffb1a88a6 100644 --- a/libs/s25main/ingameWindows/iwSettings.cpp +++ b/libs/s25main/ingameWindows/iwSettings.cpp @@ -22,7 +22,8 @@ enum ID_txtFullScreen, ID_grpFullscreen, ID_cbResolution, - ID_cbInvertMouse, + ID_txtMapScrollMode, + ID_cbMapScrollMode, ID_cbSmartCursor, ID_cbStatisticScale, }; @@ -70,8 +71,18 @@ iwSettings::iwSettings() curPos = DrawPoint(leftColOffset, curPos.y + ctrlSize.y + 5); const auto cbSize = Extent(rowWidth - curPos.x, 26); - AddCheckBox(ID_cbInvertMouse, curPos, cbSize, TextureColor::Grey, _("Invert Mouse Pan"), NormalFont, false) - ->setChecked(SETTINGS.interface.invertMouse); + + AddText(ID_txtMapScrollMode, DrawPoint(leftColOffset, curPos.y + 5), _("Map scroll mode:"), COLOR_YELLOW, + FontStyle{}, NormalFont); + ctrlComboBox* cbMapScrollMode = AddComboBox(ID_cbMapScrollMode, DrawPoint(rightColOffset, curPos.y), ctrlSize, + TextureColor::Grey, NormalFont, 100); + cbMapScrollMode->AddString( + _("Scroll same (Map moves in the same direction the mouse is moved when scrolling/panning.)")); + cbMapScrollMode->AddString( + _("Scroll opposite (Map moves in the opposite direction the mouse is moved when scrolling/panning.)")); + cbMapScrollMode->AddString(_("Grab and drag (Map moves with your cursor when scrolling/panning.)")); + cbMapScrollMode->SetSelection(static_cast(SETTINGS.interface.mapScrollMode)); + curPos.y += cbSize.y + 3; AddCheckBox(ID_cbSmartCursor, curPos, cbSize, TextureColor::Grey, _("Smart Cursor"), NormalFont, false) ->setChecked(SETTINGS.global.smartCursor) @@ -85,6 +96,9 @@ iwSettings::~iwSettings() { try { + auto* MouseMdCombo = GetCtrl(ID_cbMapScrollMode); + SETTINGS.interface.mapScrollMode = static_cast(MouseMdCombo->GetSelection().get()); + auto* SizeCombo = GetCtrl(ID_cbResolution); SETTINGS.video.fullscreenSize = video_modes[SizeCombo->GetSelection().get()]; @@ -118,7 +132,6 @@ void iwSettings::Msg_CheckboxChange(const unsigned ctrl_id, const bool checked) { switch(ctrl_id) { - case ID_cbInvertMouse: SETTINGS.interface.invertMouse = checked; break; case ID_cbSmartCursor: SETTINGS.global.smartCursor = checked; VIDEODRIVER.SetMouseWarping(checked); diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index 3195c18ab4..1ac6377d74 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -105,6 +105,19 @@ float GameWorldView::GetCurrentTargetZoomFactor() const return targetZoomFactor_; } +Position GameWorldView::ViewPosToMap(Position pos) const +{ + pos -= origin_; + if(effectiveZoomFactor_ != 1.f) + { + PointF diff(size_.x - size_.x / effectiveZoomFactor_, size_.y - size_.y / effectiveZoomFactor_); + diff /= 2.f; + pos = Position(PointF(pos) / effectiveZoomFactor_ + diff); + } + + return pos; +} + struct ObjectBetweenLines { noBase& obj; diff --git a/libs/s25main/world/GameWorldView.h b/libs/s25main/world/GameWorldView.h index 15a45578c3..9a5a3c1130 100644 --- a/libs/s25main/world/GameWorldView.h +++ b/libs/s25main/world/GameWorldView.h @@ -84,6 +84,9 @@ class GameWorldView float GetCurrentTargetZoomFactor() const; void SetNextZoomFactor(); + // Converts a view coordinate to map position + Position ViewPosToMap(Position pos) const; + /// Show or hide construction aid void ToggleShowBQ(); /// Show or hide building names diff --git a/tests/mockupDrivers/MockupVideoDriver.cpp b/tests/mockupDrivers/MockupVideoDriver.cpp index 12f5e4ebee..b9c45a6c1f 100644 --- a/tests/mockupDrivers/MockupVideoDriver.cpp +++ b/tests/mockupDrivers/MockupVideoDriver.cpp @@ -6,7 +6,8 @@ #include #include -MockupVideoDriver::MockupVideoDriver(VideoDriverLoaderInterface* CallBack) : VideoDriver(CallBack), tickCount_(1) +MockupVideoDriver::MockupVideoDriver(VideoDriverLoaderInterface* CallBack) + : VideoDriver(CallBack), tickCount_(1), numTfinger_(0) { modKeyState_.kt = KeyType::Invalid; modKeyState_.c = 0; @@ -87,3 +88,8 @@ void MockupVideoDriver::ShowErrorMessage(const std::string& title, const std::st { std::cerr << title << ": " << message << std::endl; } + +bool MockupVideoDriver::IsTouchEvent() const +{ + return numTfinger_; +} \ No newline at end of file diff --git a/tests/mockupDrivers/MockupVideoDriver.h b/tests/mockupDrivers/MockupVideoDriver.h index ea0e135b76..2a80941d3c 100644 --- a/tests/mockupDrivers/MockupVideoDriver.h +++ b/tests/mockupDrivers/MockupVideoDriver.h @@ -28,8 +28,10 @@ class MockupVideoDriver : public VideoDriver void ShowErrorMessage(const std::string& title, const std::string& message) override; using VideoDriver::FindClosestVideoMode; using VideoDriver::SetNewSize; + bool IsTouchEvent() const override; KeyEvent modKeyState_; unsigned long tickCount_; + unsigned numTfinger_; std::vector video_modes_; }; diff --git a/tests/rttrConfig/testRttrConfig.cpp b/tests/rttrConfig/testRttrConfig.cpp index 0fbacd6ae6..e645402fb0 100644 --- a/tests/rttrConfig/testRttrConfig.cpp +++ b/tests/rttrConfig/testRttrConfig.cpp @@ -59,6 +59,17 @@ BOOST_FIXTURE_TEST_CASE(PrefixPath, rttr::test::BaseFixture) } } +BOOST_FIXTURE_TEST_CASE(EnvOverride, rttr::test::BaseFixture) +{ + rttr::test::LogAccessor logAcc; + const fs::path pathOverride = fs::current_path() / "testPathOverride"; + BOOST_TEST_REQUIRE(System::setEnvVar("RTTR_GAME_DIR", pathOverride.string())); + // fakePath is just used as default path. if != pathOverride -> test failed + BOOST_TEST(RTTRCONFIG.getEnvOverride("GAME", "fakePath") == pathOverride); + RTTR_REQUIRE_LOG_CONTAINS("manually set", false); + BOOST_TEST_REQUIRE(System::removeEnvVar("RTTR_GAME_DIR")); +} + BOOST_AUTO_TEST_CASE(YearIsValid) { const std::string year = rttr::version::GetYear(); diff --git a/tests/s25Main/UI/testWindowManager.cpp b/tests/s25Main/UI/testWindowManager.cpp index 1420c6bf4a..fdb3a1741e 100644 --- a/tests/s25Main/UI/testWindowManager.cpp +++ b/tests/s25Main/UI/testWindowManager.cpp @@ -7,6 +7,7 @@ #include "Settings.h" #include "WindowManager.h" #include "desktops/Desktop.h" +#include "driver/MouseCoords.h" #include "helpers/containerUtils.h" #include "ingameWindows/IngameWindow.h" #include "ingameWindows/TransmitSettingsIgwAdapter.h" @@ -144,6 +145,75 @@ BOOST_FIXTURE_TEST_CASE(DblClick, WMFixture) mock::verify(); } +BOOST_FIXTURE_TEST_CASE(DblClickTouch, WMFixture) +{ + video->tickCount_ = 0; + video->numTfinger_ = 1; + mock::sequence s; + + MouseCoords mc1(5, 2); + mc1.ldown = true; + mc1.num_tfingers = 1; + MouseCoords mc1_u(mc1.pos); + mc1_u.num_tfingers = 1; + // Test click on distance > TOUCH_MAX_DOUBLE_CLICK_DISTANCE + MouseCoords mc2(mc1.pos.x + TOUCH_MAX_DOUBLE_CLICK_DISTANCE + 1, mc1.pos.y); + mc2.ldown = true; + mc2.num_tfingers = 1; + MouseCoords mc2_u(mc2.pos); + mc2_u.num_tfingers = 1; + // Test click on distance < TOUCH_MAX_DOUBLE_CLICK_DISTANCE + MouseCoords mc3(mc2.pos.x + TOUCH_MAX_DOUBLE_CLICK_DISTANCE - 1, mc2.pos.y); + mc3.ldown = true; + mc3.num_tfingers = 1; + MouseCoords mc3_u(mc3.pos); + mc3_u.num_tfingers = 1; + + // Set last click position on mc1 + MOCK_EXPECT(dsk->Msg_LeftDown).once().with(mc1).in(s).returns(true); + MOCK_EXPECT(dsk->Msg_LeftUp).once().with(mc1_u).in(s).returns(true); + + // Touch position mc2 -> Too far away fom previous point -> Set last click position on mc2 + MOCK_EXPECT(dsk->Msg_LeftDown).once().with(mc2).in(s).returns(true); + MOCK_EXPECT(dsk->Msg_LeftUp).once().with(mc2_u).in(s).returns(true); + + // Touch position mc3 -> In range of mc -> dblclick + MOCK_EXPECT(dsk->Msg_LeftDown).once().with(mc3).in(s).returns(true); + MouseCoords range_mc3(mc3.pos); + range_mc3.dbl_click = true; + range_mc3.num_tfingers = 1; + MOCK_EXPECT(dsk->Msg_LeftUp).once().with(range_mc3).in(s).returns(true); + + // Try to touch 3rd time -> no dblclick -> Set last click position to mc3 + MOCK_EXPECT(dsk->Msg_LeftDown).once().with(mc3).in(s).returns(true); + MOCK_EXPECT(dsk->Msg_LeftUp).once().with(mc3_u).in(s).returns(true); + + // Touch position mc3 again -> In range of mc but duration > TOUCH_DOUBLE_CLICK_INTERVAL no dblclick + MOCK_EXPECT(dsk->Msg_LeftDown).once().with(mc3).in(s).returns(true); + MOCK_EXPECT(dsk->Msg_LeftUp).once().with(mc3_u).in(s).returns(true); + + WINDOWMANAGER.Msg_LeftDown(mc1); + video->tickCount_ += 1; + WINDOWMANAGER.Msg_LeftUp(mc1_u); + + WINDOWMANAGER.Msg_LeftDown(mc2); + video->tickCount_ += 1; + WINDOWMANAGER.Msg_LeftUp(mc2_u); + + video->tickCount_ += TOUCH_DOUBLE_CLICK_INTERVAL - 1; + WINDOWMANAGER.Msg_LeftDown(mc3); + WINDOWMANAGER.Msg_LeftUp(mc3_u); + + video->tickCount_ += TOUCH_DOUBLE_CLICK_INTERVAL - 1; + WINDOWMANAGER.Msg_LeftDown(mc3); + WINDOWMANAGER.Msg_LeftUp(mc3_u); + + video->tickCount_ += TOUCH_DOUBLE_CLICK_INTERVAL; + WINDOWMANAGER.Msg_LeftDown(mc3); + WINDOWMANAGER.Msg_LeftUp(mc3_u); + mock::verify(); +} + namespace { MOCK_BASE_CLASS(TestIngameWnd, IngameWindow) { diff --git a/tests/s25Main/integration/testDskGameInterface.cpp b/tests/s25Main/integration/testDskGameInterface.cpp index 9b2d037a34..9277f03fc6 100644 --- a/tests/s25Main/integration/testDskGameInterface.cpp +++ b/tests/s25Main/integration/testDskGameInterface.cpp @@ -72,13 +72,14 @@ void checkNotScrolling(const GameWorldView& view, Cursor cursor = Cursor::Hand) BOOST_FIXTURE_TEST_CASE(Scrolling, GameInterfaceFixture) { const int acceleration = 2; - SETTINGS.interface.invertMouse = false; + SETTINGS.interface.mapScrollMode = MapScrollMode::ScrollOpposite; Position startPos(10, 15); MouseCoords mouse(startPos); + mouse.rdown = true; + // Regular scrolling: Right down, 2 moves, right up { - mouse.rdown = true; WINDOWMANAGER.Msg_RightDown(mouse); BOOST_TEST_REQUIRE(WINDOWMANAGER.GetCursor() == Cursor::Scroll); DrawPoint pos = view->GetOffset(); @@ -99,9 +100,8 @@ BOOST_FIXTURE_TEST_CASE(Scrolling, GameInterfaceFixture) checkNotScrolling(*view); } - // Inverted scrolling { - SETTINGS.interface.invertMouse = true; + SETTINGS.interface.mapScrollMode = MapScrollMode::ScrollSame; WINDOWMANAGER.Msg_RightDown(mouse); startPos = mouse.pos; BOOST_TEST_REQUIRE(WINDOWMANAGER.GetCursor() == Cursor::Scroll); @@ -112,7 +112,22 @@ BOOST_FIXTURE_TEST_CASE(Scrolling, GameInterfaceFixture) BOOST_TEST_REQUIRE(view->GetOffset() == pos - acceleration * Position(4, 3)); mouse.rdown = false; WINDOWMANAGER.Msg_RightUp(mouse); - SETTINGS.interface.invertMouse = false; + SETTINGS.interface.mapScrollMode = MapScrollMode::ScrollOpposite; + } + + { + SETTINGS.interface.mapScrollMode = MapScrollMode::GrabAndDrag; + WINDOWMANAGER.Msg_RightDown(mouse); + startPos = mouse.pos; + BOOST_TEST_REQUIRE(WINDOWMANAGER.GetCursor() == Cursor::Scroll); + const DrawPoint pos = view->GetOffset(); + mouse.pos = startPos + Position(4, 3); + WINDOWMANAGER.Msg_MouseMove(mouse); + BOOST_TEST_REQUIRE(WINDOWMANAGER.GetCursor() == Cursor::Scroll); + BOOST_TEST_REQUIRE(view->GetOffset() == pos - Position(4, 3)); + mouse.rdown = false; + WINDOWMANAGER.Msg_RightUp(mouse); + SETTINGS.interface.mapScrollMode = MapScrollMode::ScrollOpposite; } // Opening a window does not cancel scrolling