1 #include "cursesdef.h" // IWYU pragma: associated
2 #include "sdltiles.h" // IWYU pragma: associated
3
4 #include "cuboid_rectangle.h"
5 #include "point.h"
6
7 #if defined(TILES)
8
9 #include <algorithm>
10 #include <array>
11 #include <climits>
12 #include <cmath>
13 #include <cstdint>
14 #include <cstring>
15 #include <exception>
16 #include <fstream>
17 #include <iterator>
18 #include <limits>
19 #include <map>
20 #include <memory>
21 #include <set>
22 #include <stdexcept>
23 #include <type_traits>
24 #include <vector>
25 #if defined(_MSC_VER) && defined(USE_VCPKG)
26 # include <SDL2/SDL_image.h>
27 # include <SDL2/SDL_syswm.h>
28 #else
29 #ifdef _WIN32
30 # include <SDL_syswm.h>
31 #endif
32 #endif
33
34 #include "avatar.h"
35 #include "cached_options.h"
36 #include "cata_assert.h"
37 #include "cata_tiles.h"
38 #include "cata_utility.h"
39 #include "catacharset.h"
40 #include "color.h"
41 #include "color_loader.h"
42 #include "cursesport.h"
43 #include "debug.h"
44 #include "filesystem.h"
45 #include "flag.h"
46 #include "font_loader.h"
47 #include "game.h"
48 #include "game_ui.h"
49 #include "get_version.h"
50 #include "hash_utils.h"
51 #include "input.h"
52 #include "json.h"
53 #include "map.h"
54 #include "optional.h"
55 #include "options.h"
56 #include "output.h"
57 #include "path_info.h"
58 #include "point.h"
59 #include "sdl_geometry.h"
60 #include "sdl_wrappers.h"
61 #include "sdl_font.h"
62 #include "sdlsound.h"
63 #include "string_formatter.h"
64 #include "ui_manager.h"
65 #include "wcwidth.h"
66
67 #if defined(__linux__)
68 # include <cstdlib> // getenv()/setenv()
69 #endif
70
71 #if defined(_WIN32)
72 # if 1 // HACK: Hack to prevent reordering of #include "platform_win.h" by IWYU
73 # include "platform_win.h"
74 # endif
75 # include <shlwapi.h>
76 #endif
77
78 #if defined(__ANDROID__)
79 #include <jni.h>
80
81 #include "action.h"
82 #include "inventory.h"
83 #include "map.h"
84 #include "vehicle.h"
85 #include "vpart_position.h"
86 #include "worldfactory.h"
87 #endif
88
89 #define dbg(x) DebugLog((x),D_SDL) << __FILE__ << ":" << __LINE__ << ": "
90
91 //***********************************
92 //Globals *
93 //***********************************
94
95 std::unique_ptr<cata_tiles> tilecontext;
96 static uint32_t lastupdate = 0;
97 static uint32_t interval = 25;
98 static bool needupdate = false;
99 static bool need_invalidate_framebuffers = false;
100
101 palette_array windowsPalette;
102
103 static Font_Ptr font;
104 static Font_Ptr map_font;
105 static Font_Ptr overmap_font;
106
107 static SDL_Window_Ptr window;
108 static SDL_Renderer_Ptr renderer;
109 static SDL_PixelFormat_Ptr format;
110 static SDL_Texture_Ptr display_buffer;
111 static GeometryRenderer_Ptr geometry;
112 #if defined(__ANDROID__)
113 static SDL_Texture_Ptr touch_joystick;
114 #endif
115 static int WindowWidth; //Width of the actual window, not the curses window
116 static int WindowHeight; //Height of the actual window, not the curses window
117 // input from various input sources. Each input source sets the type and
118 // the actual input value (key pressed, mouse button clicked, ...)
119 // This value is finally returned by input_manager::get_input_event.
120 static input_event last_input;
121
122 static constexpr int ERR = -1;
123 static int inputdelay; //How long getch will wait for a character to be typed
124 static Uint32 delaydpad =
125 std::numeric_limits<Uint32>::max(); // Used for entering diagonal directions with d-pad.
126 static Uint32 dpad_delay =
127 100; // Delay in milliseconds between registering a d-pad event and processing it.
128 static bool dpad_continuous = false; // Whether we're currently moving continuously with the dpad.
129 static int lastdpad = ERR; // Keeps track of the last dpad press.
130 static int queued_dpad = ERR; // Queued dpad press, for individual button presses.
131 int fontwidth; //the width of the font, background is always this size
132 int fontheight; //the height of the font, background is always this size
133 static int TERMINAL_WIDTH;
134 static int TERMINAL_HEIGHT;
135 static bool fullscreen;
136 static int scaling_factor;
137
138 static SDL_Joystick *joystick; // Only one joystick for now.
139
140 using cata_cursesport::curseline;
141 using cata_cursesport::cursecell;
142 static std::vector<curseline> oversized_framebuffer;
143 static std::vector<curseline> terminal_framebuffer;
144 static std::weak_ptr<void> winBuffer; //tracking last drawn window to fix the framebuffer
145 static int fontScaleBuffer; //tracking zoom levels to fix framebuffer w/tiles
146
147 //***********************************
148 //Non-curses, Window functions *
149 //***********************************
150
operator ==(const cata_cursesport::WINDOW * const lhs,const catacurses::window & rhs)151 static bool operator==( const cata_cursesport::WINDOW *const lhs, const catacurses::window &rhs )
152 {
153 return lhs == rhs.get();
154 }
155
ClearScreen()156 static void ClearScreen()
157 {
158 SetRenderDrawColor( renderer, 0, 0, 0, 255 );
159 RenderClear( renderer );
160 }
161
InitSDL()162 static void InitSDL()
163 {
164 int init_flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER;
165 int ret;
166
167 #if defined(SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING)
168 SDL_SetHint( SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING, "1" );
169 #endif
170
171 #if defined(__linux__)
172 // https://bugzilla.libsdl.org/show_bug.cgi?id=3472#c5
173 if( SDL_COMPILEDVERSION == SDL_VERSIONNUM( 2, 0, 5 ) ) {
174 const char *xmod = getenv( "XMODIFIERS" );
175 if( xmod && strstr( xmod, "@im=ibus" ) != nullptr ) {
176 setenv( "XMODIFIERS", "@im=none", 1 );
177 }
178 }
179 #endif
180
181 ret = SDL_Init( init_flags );
182 throwErrorIf( ret != 0, "SDL_Init failed" );
183
184 ret = TTF_Init();
185 throwErrorIf( ret != 0, "TTF_Init failed" );
186
187 // cata_tiles won't be able to load the tiles, but the normal SDL
188 // code will display fine.
189 ret = IMG_Init( IMG_INIT_PNG );
190 printErrorIf( ( ret & IMG_INIT_PNG ) != IMG_INIT_PNG,
191 "IMG_Init failed to initialize PNG support, tiles won't work" );
192
193 ret = SDL_InitSubSystem( SDL_INIT_JOYSTICK );
194 printErrorIf( ret != 0, "Initializing joystick subsystem failed" );
195
196 //SDL2 has no functionality for INPUT_DELAY, we would have to query it manually, which is expensive
197 //SDL2 instead uses the OS's Input Delay.
198
199 atexit( SDL_Quit );
200 }
201
SetupRenderTarget()202 static bool SetupRenderTarget()
203 {
204 SetRenderDrawBlendMode( renderer, SDL_BLENDMODE_NONE );
205 display_buffer.reset( SDL_CreateTexture( renderer.get(), SDL_PIXELFORMAT_ARGB8888,
206 SDL_TEXTUREACCESS_TARGET, WindowWidth / scaling_factor, WindowHeight / scaling_factor ) );
207 if( printErrorIf( !display_buffer, "Failed to create window buffer" ) ) {
208 return false;
209 }
210 if( printErrorIf( SDL_SetRenderTarget( renderer.get(), display_buffer.get() ) != 0,
211 "SDL_SetRenderTarget failed" ) ) {
212 return false;
213 }
214 ClearScreen();
215
216 return true;
217 }
218
219 //Registers, creates, and shows the Window!!
WinCreate()220 static void WinCreate()
221 {
222 std::string version = string_format( "Cataclysm: Dark Days Ahead - %s", getVersionString() );
223
224 // Common flags used for fulscreen and for windowed
225 int window_flags = 0;
226 WindowWidth = TERMINAL_WIDTH * fontwidth * scaling_factor;
227 WindowHeight = TERMINAL_HEIGHT * fontheight * scaling_factor;
228 window_flags |= SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
229
230 if( get_option<std::string>( "SCALING_MODE" ) != "none" ) {
231 SDL_SetHint( SDL_HINT_RENDER_SCALE_QUALITY, get_option<std::string>( "SCALING_MODE" ).c_str() );
232 }
233
234 #if !defined(__ANDROID__)
235 if( get_option<std::string>( "FULLSCREEN" ) == "fullscreen" ) {
236 window_flags |= SDL_WINDOW_FULLSCREEN;
237 fullscreen = true;
238 SDL_SetHint( SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0" );
239 } else if( get_option<std::string>( "FULLSCREEN" ) == "windowedbl" ) {
240 window_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
241 fullscreen = true;
242 SDL_SetHint( SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0" );
243 } else if( get_option<std::string>( "FULLSCREEN" ) == "maximized" ) {
244 window_flags |= SDL_WINDOW_MAXIMIZED;
245 }
246 #endif
247
248 int display = std::stoi( get_option<std::string>( "DISPLAY" ) );
249 if( display < 0 || display >= SDL_GetNumVideoDisplays() ) {
250 display = 0;
251 }
252
253 #if defined(__ANDROID__)
254 // Bugfix for red screen on Samsung S3/Mali
255 // https://forums.libsdl.org/viewtopic.php?t=11445
256 SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 5 );
257 SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 6 );
258 SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 5 );
259
260 // Fix Back button crash on Android 9
261 #if defined(SDL_HINT_ANDROID_TRAP_BACK_BUTTON )
262 const bool trap_back_button = get_option<bool>( "ANDROID_TRAP_BACK_BUTTON" );
263 SDL_SetHint( SDL_HINT_ANDROID_TRAP_BACK_BUTTON, trap_back_button ? "1" : "0" );
264 #endif
265
266 // Prevent mouse|touch input confusion
267 #if defined(SDL_HINT_ANDROID_SEPARATE_MOUSE_AND_TOUCH)
268 SDL_SetHint( SDL_HINT_ANDROID_SEPARATE_MOUSE_AND_TOUCH, "1" );
269 #else
270 SDL_SetHint( SDL_HINT_MOUSE_TOUCH_EVENTS, "0" );
271 SDL_SetHint( SDL_HINT_TOUCH_MOUSE_EVENTS, "0" );
272 #endif
273 #endif
274
275 ::window.reset( SDL_CreateWindow( version.c_str(),
276 SDL_WINDOWPOS_CENTERED_DISPLAY( display ),
277 SDL_WINDOWPOS_CENTERED_DISPLAY( display ),
278 WindowWidth,
279 WindowHeight,
280 window_flags
281 ) );
282 throwErrorIf( !::window, "SDL_CreateWindow failed" );
283
284 #if !defined(__ANDROID__)
285 // On Android SDL seems janky in windowed mode so we're fullscreen all the time.
286 // Fullscreen mode is now modified so it obeys terminal width/height, rather than
287 // overwriting it with this calculation.
288 if( window_flags & SDL_WINDOW_FULLSCREEN || window_flags & SDL_WINDOW_FULLSCREEN_DESKTOP
289 || window_flags & SDL_WINDOW_MAXIMIZED ) {
290 SDL_GetWindowSize( ::window.get(), &WindowWidth, &WindowHeight );
291 // Ignore previous values, use the whole window, but nothing more.
292 TERMINAL_WIDTH = WindowWidth / fontwidth / scaling_factor;
293 TERMINAL_HEIGHT = WindowHeight / fontheight / scaling_factor;
294 }
295 #endif
296 // Initialize framebuffer caches
297 terminal_framebuffer.resize( TERMINAL_HEIGHT );
298 for( int i = 0; i < TERMINAL_HEIGHT; i++ ) {
299 terminal_framebuffer[i].chars.assign( TERMINAL_WIDTH, cursecell( "" ) );
300 }
301
302 oversized_framebuffer.resize( TERMINAL_HEIGHT );
303 for( int i = 0; i < TERMINAL_HEIGHT; i++ ) {
304 oversized_framebuffer[i].chars.assign( TERMINAL_WIDTH, cursecell( "" ) );
305 }
306
307 const Uint32 wformat = SDL_GetWindowPixelFormat( ::window.get() );
308 format.reset( SDL_AllocFormat( wformat ) );
309 throwErrorIf( !format, "SDL_AllocFormat failed" );
310
311 int renderer_id = -1;
312 #if !defined(__ANDROID__)
313 bool software_renderer = get_option<std::string>( "RENDERER" ).empty();
314 std::string renderer_name;
315 if( software_renderer ) {
316 renderer_name = "software";
317 } else {
318 renderer_name = get_option<std::string>( "RENDERER" );
319 }
320
321 if( renderer_name == "direct3d" ) {
322 direct3d_mode = true;
323 }
324
325 const int numRenderDrivers = SDL_GetNumRenderDrivers();
326 for( int i = 0; i < numRenderDrivers; i++ ) {
327 SDL_RendererInfo ri;
328 SDL_GetRenderDriverInfo( i, &ri );
329 if( renderer_name == ri.name ) {
330 renderer_id = i;
331 DebugLog( D_INFO, DC_ALL ) << "Active renderer: " << renderer_id << "/" << ri.name;
332 break;
333 }
334 }
335 #else
336 bool software_renderer = get_option<bool>( "SOFTWARE_RENDERING" );
337 #endif
338
339 #if defined(SDL_HINT_RENDER_BATCHING)
340 SDL_SetHint( SDL_HINT_RENDER_BATCHING, get_option<bool>( "RENDER_BATCHING" ) ? "1" : "0" );
341 #endif
342 if( !software_renderer ) {
343 dbg( D_INFO ) << "Attempting to initialize accelerated SDL renderer.";
344
345 renderer.reset( SDL_CreateRenderer( ::window.get(), renderer_id, SDL_RENDERER_ACCELERATED |
346 SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_TARGETTEXTURE ) );
347 if( printErrorIf( !renderer,
348 "Failed to initialize accelerated renderer, falling back to software rendering" ) ) {
349 software_renderer = true;
350 } else if( !SetupRenderTarget() ) {
351 dbg( D_ERROR ) <<
352 "Failed to initialize display buffer under accelerated rendering, falling back to software rendering.";
353 software_renderer = true;
354 display_buffer.reset();
355 renderer.reset();
356 }
357 }
358
359 if( software_renderer ) {
360 if( get_option<bool>( "FRAMEBUFFER_ACCEL" ) ) {
361 SDL_SetHint( SDL_HINT_FRAMEBUFFER_ACCELERATION, "1" );
362 }
363 renderer.reset( SDL_CreateRenderer( ::window.get(), -1,
364 SDL_RENDERER_SOFTWARE | SDL_RENDERER_TARGETTEXTURE ) );
365 throwErrorIf( !renderer, "Failed to initialize software renderer" );
366 throwErrorIf( !SetupRenderTarget(),
367 "Failed to initialize display buffer under software rendering, unable to continue." );
368 }
369
370 SDL_SetWindowMinimumSize( ::window.get(), fontwidth * FULL_SCREEN_WIDTH * scaling_factor,
371 fontheight * FULL_SCREEN_HEIGHT * scaling_factor );
372
373 #if defined(__ANDROID__)
374 // TODO: Not too sure why this works to make fullscreen on Android behave. :/
375 if( window_flags & SDL_WINDOW_FULLSCREEN || window_flags & SDL_WINDOW_FULLSCREEN_DESKTOP
376 || window_flags & SDL_WINDOW_MAXIMIZED ) {
377 SDL_GetWindowSize( ::window.get(), &WindowWidth, &WindowHeight );
378 }
379
380 // Load virtual joystick texture
381 touch_joystick = CreateTextureFromSurface( renderer, load_image( "android/joystick.png" ) );
382 #endif
383
384 ClearScreen();
385
386 // Errors here are ignored, worst case: the option does not work as expected,
387 // but that won't crash
388 if( get_option<std::string>( "HIDE_CURSOR" ) != "show" && SDL_ShowCursor( -1 ) ) {
389 SDL_ShowCursor( SDL_DISABLE );
390 } else {
391 SDL_ShowCursor( SDL_ENABLE );
392 }
393
394 // Initialize joysticks.
395 int numjoy = SDL_NumJoysticks();
396
397 if( get_option<bool>( "ENABLE_JOYSTICK" ) && numjoy >= 1 ) {
398 if( numjoy > 1 ) {
399 dbg( D_WARNING ) <<
400 "You have more than one gamepads/joysticks plugged in, only the first will be used.";
401 }
402 joystick = SDL_JoystickOpen( 0 );
403 printErrorIf( joystick == nullptr, "SDL_JoystickOpen failed" );
404 if( joystick ) {
405 printErrorIf( SDL_JoystickEventState( SDL_ENABLE ) < 0,
406 "SDL_JoystickEventState(SDL_ENABLE) failed" );
407 }
408 } else {
409 joystick = nullptr;
410 }
411
412 // Set up audio mixer.
413 init_sound();
414
415 DebugLog( D_INFO, DC_ALL ) << "USE_COLOR_MODULATED_TEXTURES is set to " <<
416 get_option<bool>( "USE_COLOR_MODULATED_TEXTURES" );
417 //initialize the alternate rectangle texture for replacing SDL_RenderFillRect
418 if( get_option<bool>( "USE_COLOR_MODULATED_TEXTURES" ) && !software_renderer ) {
419 geometry = std::make_unique<ColorModulatedGeometryRenderer>( renderer );
420 } else {
421 geometry = std::make_unique<DefaultGeometryRenderer>();
422 }
423 }
424
WinDestroy()425 static void WinDestroy()
426 {
427 #if defined(__ANDROID__)
428 touch_joystick.reset();
429 #endif
430
431 shutdown_sound();
432 tilecontext.reset();
433
434 if( joystick ) {
435 SDL_JoystickClose( joystick );
436
437 joystick = nullptr;
438 }
439 geometry.reset();
440 format.reset();
441 display_buffer.reset();
442 renderer.reset();
443 ::window.reset();
444 }
445
446 /// Converts a color from colorscheme to SDL_Color.
color_as_sdl(const unsigned char color)447 static inline const SDL_Color &color_as_sdl( const unsigned char color )
448 {
449 return windowsPalette[color];
450 }
451
452 #if defined(__ANDROID__)
453 void draw_terminal_size_preview();
454 void draw_quick_shortcuts();
455 void draw_virtual_joystick();
456
457 static bool quick_shortcuts_enabled = true;
458
459 // For previewing the terminal size with a transparent rectangle overlay when user is adjusting it in the settings
460 static int preview_terminal_width = -1;
461 static int preview_terminal_height = -1;
462 static uint32_t preview_terminal_change_time = 0;
463
464 extern "C" {
465
466 static bool visible_display_frame_dirty = false;
467 static bool has_visible_display_frame = false;
468 static SDL_Rect visible_display_frame;
469
Java_org_libsdl_app_SDLActivity_onNativeVisibleDisplayFrameChanged(JNIEnv * env,jclass jcls,jint left,jint top,jint right,jint bottom)470 JNIEXPORT void JNICALL Java_org_libsdl_app_SDLActivity_onNativeVisibleDisplayFrameChanged(
471 JNIEnv *env, jclass jcls, jint left, jint top, jint right, jint bottom )
472 {
473 ( void )env; // unused
474 ( void )jcls; // unused
475 has_visible_display_frame = true;
476 visible_display_frame_dirty = true;
477 visible_display_frame.x = left;
478 visible_display_frame.y = top;
479 visible_display_frame.w = right - left;
480 visible_display_frame.h = bottom - top;
481 }
482
483 } // "C"
484
get_android_render_rect(float DisplayBufferWidth,float DisplayBufferHeight)485 SDL_Rect get_android_render_rect( float DisplayBufferWidth, float DisplayBufferHeight )
486 {
487 // If the display buffer aspect ratio is wider than the display,
488 // draw it at the top of the screen so it doesn't get covered up
489 // by the virtual keyboard. Otherwise just center it.
490 SDL_Rect dstrect;
491 float DisplayBufferAspect = DisplayBufferWidth / static_cast<float>( DisplayBufferHeight );
492 float WindowHeightLessShortcuts = static_cast<float>( WindowHeight );
493 if( !get_option<bool>( "ANDROID_SHORTCUT_OVERLAP" ) && quick_shortcuts_enabled ) {
494 WindowHeightLessShortcuts -= get_option<int>( "ANDROID_SHORTCUT_HEIGHT" );
495 }
496 float WindowAspect = WindowWidth / static_cast<float>( WindowHeightLessShortcuts );
497 if( WindowAspect < DisplayBufferAspect ) {
498 dstrect.x = 0;
499 dstrect.y = 0;
500 dstrect.w = WindowWidth;
501 dstrect.h = WindowWidth / DisplayBufferAspect;
502 } else {
503 dstrect.x = 0.5f * ( WindowWidth - ( WindowHeightLessShortcuts * DisplayBufferAspect ) );
504 dstrect.y = 0;
505 dstrect.w = WindowHeightLessShortcuts * DisplayBufferAspect;
506 dstrect.h = WindowHeightLessShortcuts;
507 }
508
509 // Make sure the destination rectangle fits within the visible area
510 if( get_option<bool>( "ANDROID_KEYBOARD_SCREEN_SCALE" ) && has_visible_display_frame ) {
511 int vdf_right = visible_display_frame.x + visible_display_frame.w;
512 int vdf_bottom = visible_display_frame.y + visible_display_frame.h;
513 if( vdf_right < dstrect.x + dstrect.w ) {
514 dstrect.w = vdf_right - dstrect.x;
515 }
516 if( vdf_bottom < dstrect.y + dstrect.h ) {
517 dstrect.h = vdf_bottom - dstrect.y;
518 }
519 }
520 return dstrect;
521 }
522
523 #endif
524
refresh_display()525 void refresh_display()
526 {
527 needupdate = false;
528 lastupdate = SDL_GetTicks();
529
530 if( test_mode ) {
531 return;
532 }
533
534 // Select default target (the window), copy rendered buffer
535 // there, present it, select the buffer as target again.
536 SetRenderTarget( renderer, nullptr );
537 #if defined(__ANDROID__)
538 SDL_Rect dstrect = get_android_render_rect( TERMINAL_WIDTH * fontwidth,
539 TERMINAL_HEIGHT * fontheight );
540 SetRenderDrawColor( renderer, 0, 0, 0, 255 );
541 RenderClear( renderer );
542 RenderCopy( renderer, display_buffer, NULL, &dstrect );
543 #else
544 RenderCopy( renderer, display_buffer, nullptr, nullptr );
545 #endif
546 #if defined(__ANDROID__)
547 draw_terminal_size_preview();
548 if( g ) {
549 draw_quick_shortcuts();
550 }
551 draw_virtual_joystick();
552 #endif
553 SDL_RenderPresent( renderer.get() );
554 SetRenderTarget( renderer, display_buffer );
555 }
556
557 // only update if the set interval has elapsed
try_sdl_update()558 static void try_sdl_update()
559 {
560 uint32_t now = SDL_GetTicks();
561 if( now - lastupdate >= interval ) {
562 refresh_display();
563 } else {
564 needupdate = true;
565 }
566 }
567
568 //for resetting the render target after updating texture caches in cata_tiles.cpp
set_displaybuffer_rendertarget()569 void set_displaybuffer_rendertarget()
570 {
571 SetRenderTarget( renderer, display_buffer );
572 }
573
invalidate_framebuffer(std::vector<curseline> & framebuffer,const point & p,int width,int height)574 static void invalidate_framebuffer( std::vector<curseline> &framebuffer, const point &p, int width,
575 int height )
576 {
577 for( int j = 0, fby = p.y; j < height; j++, fby++ ) {
578 std::fill_n( framebuffer[fby].chars.begin() + p.x, width, cursecell( "" ) );
579 }
580 }
581
invalidate_framebuffer(std::vector<curseline> & framebuffer)582 static void invalidate_framebuffer( std::vector<curseline> &framebuffer )
583 {
584 for( curseline &i : framebuffer ) {
585 std::fill_n( i.chars.begin(), i.chars.size(), cursecell( "" ) );
586 }
587 }
588
reinitialize_framebuffer()589 void reinitialize_framebuffer()
590 {
591 static int prev_height = -1;
592 static int prev_width = -1;
593 //Re-initialize the framebuffer with new values.
594 const int new_height = std::max( { TERMY, OVERMAP_WINDOW_HEIGHT, TERRAIN_WINDOW_HEIGHT } );
595 const int new_width = std::max( { TERMX, OVERMAP_WINDOW_WIDTH, TERRAIN_WINDOW_WIDTH } );
596 if( new_height != prev_height || new_width != prev_width ) {
597 prev_height = new_height;
598 prev_width = new_width;
599 oversized_framebuffer.resize( new_height );
600 for( int i = 0; i < new_height; i++ ) {
601 oversized_framebuffer[i].chars.assign( new_width, cursecell( "" ) );
602 }
603 terminal_framebuffer.resize( new_height );
604 for( int i = 0; i < new_height; i++ ) {
605 terminal_framebuffer[i].chars.assign( new_width, cursecell( "" ) );
606 }
607 } else if( need_invalidate_framebuffers ) {
608 need_invalidate_framebuffers = false;
609 invalidate_framebuffer( oversized_framebuffer );
610 invalidate_framebuffer( terminal_framebuffer );
611 }
612 }
613
invalidate_framebuffer_proportion(cata_cursesport::WINDOW * win)614 static void invalidate_framebuffer_proportion( cata_cursesport::WINDOW *win )
615 {
616 const int oversized_width = std::max( TERMX, std::max( OVERMAP_WINDOW_WIDTH,
617 TERRAIN_WINDOW_WIDTH ) );
618 const int oversized_height = std::max( TERMY, std::max( OVERMAP_WINDOW_HEIGHT,
619 TERRAIN_WINDOW_HEIGHT ) );
620
621 // check if the framebuffers/windows have been prepared yet
622 if( oversized_height == 0 || oversized_width == 0 ) {
623 return;
624 }
625 if( !g || win == nullptr ) {
626 return;
627 }
628 if( win == g->w_overmap || win == g->w_terrain ) {
629 return;
630 }
631
632 // track the dimensions for conversion
633 const point termpixel( win->pos.x * font->width, win->pos.y * font->height );
634 const int termpixel_x2 = termpixel.x + win->width * font->width - 1;
635 const int termpixel_y2 = termpixel.y + win->height * font->height - 1;
636
637 if( map_font != nullptr && map_font->width != 0 && map_font->height != 0 ) {
638 const int mapfont_x = termpixel.x / map_font->width;
639 const int mapfont_y = termpixel.y / map_font->height;
640 const int mapfont_x2 = std::min( termpixel_x2 / map_font->width, oversized_width - 1 );
641 const int mapfont_y2 = std::min( termpixel_y2 / map_font->height, oversized_height - 1 );
642 const int mapfont_width = mapfont_x2 - mapfont_x + 1;
643 const int mapfont_height = mapfont_y2 - mapfont_y + 1;
644 invalidate_framebuffer( oversized_framebuffer, point( mapfont_x, mapfont_y ), mapfont_width,
645 mapfont_height );
646 }
647
648 if( overmap_font != nullptr && overmap_font->width != 0 && overmap_font->height != 0 ) {
649 const int overmapfont_x = termpixel.x / overmap_font->width;
650 const int overmapfont_y = termpixel.y / overmap_font->height;
651 const int overmapfont_x2 = std::min( termpixel_x2 / overmap_font->width, oversized_width - 1 );
652 const int overmapfont_y2 = std::min( termpixel_y2 / overmap_font->height,
653 oversized_height - 1 );
654 const int overmapfont_width = overmapfont_x2 - overmapfont_x + 1;
655 const int overmapfont_height = overmapfont_y2 - overmapfont_y + 1;
656 invalidate_framebuffer( oversized_framebuffer, point( overmapfont_x, overmapfont_y ),
657 overmapfont_width,
658 overmapfont_height );
659 }
660 }
661
662 // clear the framebuffer when werase is called on certain windows that don't use the main terminal font
handle_additional_window_clear(WINDOW * win)663 void cata_cursesport::handle_additional_window_clear( WINDOW *win )
664 {
665 if( !g ) {
666 return;
667 }
668 if( win == g->w_terrain || win == g->w_overmap ) {
669 invalidate_framebuffer( oversized_framebuffer );
670 }
671 }
672
clear_window_area(const catacurses::window & win_)673 void clear_window_area( const catacurses::window &win_ )
674 {
675 cata_cursesport::WINDOW *const win = win_.get<cata_cursesport::WINDOW>();
676 geometry->rect( renderer, point( win->pos.x * fontwidth, win->pos.y * fontheight ),
677 win->width * fontwidth, win->height * fontheight, color_as_sdl( catacurses::black ) );
678 }
679
draw_window(Font_Ptr & font,const catacurses::window & w,const point & offset)680 static bool draw_window( Font_Ptr &font, const catacurses::window &w, const point &offset )
681 {
682 if( scaling_factor > 1 ) {
683 SDL_RenderSetLogicalSize( renderer.get(), WindowWidth / scaling_factor,
684 WindowHeight / scaling_factor );
685 }
686
687 cata_cursesport::WINDOW *const win = w.get<cata_cursesport::WINDOW>();
688 //Keeping track of the last drawn window
689 const cata_cursesport::WINDOW *winBuffer = static_cast<cata_cursesport::WINDOW *>
690 ( ::winBuffer.lock().get() );
691 if( !fontScaleBuffer ) {
692 fontScaleBuffer = tilecontext->get_tile_width();
693 }
694 const int fontScale = tilecontext->get_tile_width();
695 //This creates a problem when map_font is different from the regular font
696 //Specifically when showing the overmap
697 //And in some instances of screen change, i.e. inventory.
698 bool oldWinCompatible = false;
699
700 // clear the oversized buffer proportionally
701 invalidate_framebuffer_proportion( win );
702
703 // use the oversize buffer when dealing with windows that can have a different font than the main text font
704 bool use_oversized_framebuffer = g && ( w == g->w_terrain || w == g->w_overmap );
705
706 std::vector<curseline> &framebuffer = use_oversized_framebuffer ? oversized_framebuffer :
707 terminal_framebuffer;
708
709 /*
710 Let's try to keep track of different windows.
711 A number of windows are coexisting on the screen, so don't have to interfere.
712
713 g->w_terrain, g->w_minimap, g->w_HP, g->w_status, g->w_status2, g->w_messages,
714 g->w_location, and g->w_minimap, can be buffered if either of them was
715 the previous window.
716
717 g->w_overmap and g->w_omlegend are likewise.
718
719 Everything else works on strict equality because there aren't yet IDs for some of them.
720 */
721 if( g && ( w == g->w_terrain || w == g->w_minimap ) ) {
722 if( winBuffer == g->w_terrain || winBuffer == g->w_minimap ) {
723 oldWinCompatible = true;
724 }
725 } else if( g && ( w == g->w_overmap || w == g->w_omlegend ) ) {
726 if( winBuffer == g->w_overmap || winBuffer == g->w_omlegend ) {
727 oldWinCompatible = true;
728 }
729 } else {
730 if( win == winBuffer ) {
731 oldWinCompatible = true;
732 }
733 }
734
735 // TODO: Get this from UTF system to make sure it is exactly the kind of space we need
736 static const std::string space_string = " ";
737
738 bool update = false;
739 for( int j = 0; j < win->height; j++ ) {
740 if( !win->line[j].touched ) {
741 continue;
742 }
743
744 const int fby = win->pos.y + j;
745 if( fby >= static_cast<int>( framebuffer.size() ) ) {
746 // prevent indexing outside the frame buffer. This might happen for some parts of the window. FIX #28953.
747 break;
748 }
749
750 update = true;
751 win->line[j].touched = false;
752 for( int i = 0; i < win->width; i++ ) {
753 const int fbx = win->pos.x + i;
754 if( fbx >= static_cast<int>( framebuffer[fby].chars.size() ) ) {
755 // prevent indexing outside the frame buffer. This might happen for some parts of the window.
756 break;
757 }
758
759 const cursecell &cell = win->line[j].chars[i];
760
761 const int drawx = offset.x + i * font->width;
762 const int drawy = offset.y + j * font->height;
763 if( drawx + font->width > WindowWidth || drawy + font->height > WindowHeight ) {
764 // Outside of the display area, would not render anyway
765 continue;
766 }
767
768 // Avoid redrawing an unchanged tile by checking the framebuffer cache
769 // TODO: handle caching when drawing normal windows over graphical tiles
770 cursecell &oldcell = framebuffer[fby].chars[fbx];
771
772 if( oldWinCompatible && cell == oldcell && fontScale == fontScaleBuffer ) {
773 continue;
774 }
775 oldcell = cell;
776
777 if( cell.ch.empty() ) {
778 continue; // second cell of a multi-cell character
779 }
780
781 // Spaces are used a lot, so this does help noticeably
782 if( cell.ch == space_string ) {
783 geometry->rect( renderer, point( drawx, drawy ), font->width, font->height,
784 color_as_sdl( cell.BG ) );
785 continue;
786 }
787 const int codepoint = UTF8_getch( cell.ch );
788 const catacurses::base_color FG = cell.FG;
789 const catacurses::base_color BG = cell.BG;
790 int cw = ( codepoint == UNKNOWN_UNICODE ) ? 1 : utf8_width( cell.ch );
791 if( cw < 1 ) {
792 // utf8_width() may return a negative width
793 continue;
794 }
795 bool use_draw_ascii_lines_routine = get_option<bool>( "USE_DRAW_ASCII_LINES_ROUTINE" );
796 unsigned char uc = static_cast<unsigned char>( cell.ch[0] );
797 switch( codepoint ) {
798 case LINE_XOXO_UNICODE:
799 uc = LINE_XOXO_C;
800 break;
801 case LINE_OXOX_UNICODE:
802 uc = LINE_OXOX_C;
803 break;
804 case LINE_XXOO_UNICODE:
805 uc = LINE_XXOO_C;
806 break;
807 case LINE_OXXO_UNICODE:
808 uc = LINE_OXXO_C;
809 break;
810 case LINE_OOXX_UNICODE:
811 uc = LINE_OOXX_C;
812 break;
813 case LINE_XOOX_UNICODE:
814 uc = LINE_XOOX_C;
815 break;
816 case LINE_XXXO_UNICODE:
817 uc = LINE_XXXO_C;
818 break;
819 case LINE_XXOX_UNICODE:
820 uc = LINE_XXOX_C;
821 break;
822 case LINE_XOXX_UNICODE:
823 uc = LINE_XOXX_C;
824 break;
825 case LINE_OXXX_UNICODE:
826 uc = LINE_OXXX_C;
827 break;
828 case LINE_XXXX_UNICODE:
829 uc = LINE_XXXX_C;
830 break;
831 case UNKNOWN_UNICODE:
832 use_draw_ascii_lines_routine = true;
833 break;
834 default:
835 use_draw_ascii_lines_routine = false;
836 break;
837 }
838 geometry->rect( renderer, point( drawx, drawy ), font->width * cw, font->height,
839 color_as_sdl( BG ) );
840 if( use_draw_ascii_lines_routine ) {
841 font->draw_ascii_lines( renderer, geometry, uc, point( drawx, drawy ), FG );
842 } else {
843 font->OutputChar( renderer, geometry, cell.ch, point( drawx, drawy ), FG );
844 }
845 }
846 }
847 win->draw = false; //We drew the window, mark it as so
848 //Keeping track of last drawn window and tilemode zoom level
849 ::winBuffer = w.weak_ptr();
850 fontScaleBuffer = tilecontext->get_tile_width();
851
852 return update;
853 }
854
draw_window(Font_Ptr & font,const catacurses::window & w)855 static bool draw_window( Font_Ptr &font, const catacurses::window &w )
856 {
857 cata_cursesport::WINDOW *const win = w.get<cata_cursesport::WINDOW>();
858 // Use global font sizes here to make this independent of the
859 // font used for this window.
860 return draw_window( font, w, point( win->pos.x * ::fontwidth, win->pos.y * ::fontheight ) );
861 }
862
curses_drawwindow(const catacurses::window & w)863 void cata_cursesport::curses_drawwindow( const catacurses::window &w )
864 {
865 if( scaling_factor > 1 ) {
866 SDL_RenderSetLogicalSize( renderer.get(), WindowWidth / scaling_factor,
867 WindowHeight / scaling_factor );
868 }
869 WINDOW *const win = w.get<WINDOW>();
870 bool update = false;
871 if( g && w == g->w_terrain && use_tiles ) {
872 // color blocks overlay; drawn on top of tiles and on top of overlay strings (if any).
873 color_block_overlay_container color_blocks;
874
875 // Strings with colors do be drawn with map_font on top of tiles.
876 std::multimap<point, formatted_text> overlay_strings;
877
878 // game::w_terrain can be drawn by the tilecontext.
879 // skip the normal drawing code for it.
880 tilecontext->draw(
881 point( win->pos.x * fontwidth, win->pos.y * fontheight ),
882 g->ter_view_p,
883 TERRAIN_WINDOW_TERM_WIDTH * font->width,
884 TERRAIN_WINDOW_TERM_HEIGHT * font->height,
885 overlay_strings,
886 color_blocks );
887
888 // color blocks overlay
889 if( !color_blocks.second.empty() ) {
890 SDL_BlendMode blend_mode;
891 GetRenderDrawBlendMode( renderer, blend_mode ); // save the current blend mode
892 SetRenderDrawBlendMode( renderer, color_blocks.first ); // set the new blend mode
893 for( const auto &e : color_blocks.second ) {
894 geometry->rect( renderer, e.first, tilecontext->get_tile_width(),
895 tilecontext->get_tile_height(), e.second );
896 }
897 SetRenderDrawBlendMode( renderer, blend_mode ); // set the old blend mode
898 }
899
900 // overlay strings
901 point prev_coord;
902 int x_offset = 0;
903 int alignment_offset = 0;
904 for( const auto &iter : overlay_strings ) {
905 const point coord = iter.first;
906 const formatted_text ft = iter.second;
907 const utf8_wrapper text( ft.text );
908
909 // Strings at equal coords are displayed sequentially.
910 if( coord != prev_coord ) {
911 x_offset = 0;
912 }
913
914 // Calculate length of all strings in sequence to align them.
915 if( x_offset == 0 ) {
916 int full_text_length = 0;
917 const auto range = overlay_strings.equal_range( coord );
918 for( auto ri = range.first; ri != range.second; ++ri ) {
919 utf8_wrapper rt( ri->second.text );
920 full_text_length += rt.display_width();
921 }
922
923 alignment_offset = 0;
924 if( ft.alignment == text_alignment::center ) {
925 alignment_offset = full_text_length / 2;
926 } else if( ft.alignment == text_alignment::right ) {
927 alignment_offset = full_text_length - 1;
928 }
929 }
930
931 int width = 0;
932 for( size_t i = 0; i < text.size(); ++i ) {
933 const int x0 = win->pos.x * fontwidth;
934 const int y0 = win->pos.y * fontheight;
935 const int x = x0 + ( x_offset - alignment_offset + width ) * map_font->width + coord.x;
936 const int y = y0 + coord.y;
937
938 // Clip to window bounds.
939 if( x < x0 || x > x0 + ( TERRAIN_WINDOW_TERM_WIDTH - 1 ) * font->width
940 || y < y0 || y > y0 + ( TERRAIN_WINDOW_TERM_HEIGHT - 1 ) * font->height ) {
941 continue;
942 }
943
944 // TODO: draw with outline / BG color for better readability
945 const uint32_t ch = text.at( i );
946 map_font->OutputChar( renderer, geometry, utf32_to_utf8( ch ), point( x, y ), ft.color );
947 width += mk_wcwidth( ch );
948 }
949
950 prev_coord = coord;
951 x_offset = width;
952 }
953
954 invalidate_framebuffer( terminal_framebuffer, win->pos,
955 TERRAIN_WINDOW_TERM_WIDTH, TERRAIN_WINDOW_TERM_HEIGHT );
956
957 update = true;
958 } else if( g && w == g->w_terrain && map_font ) {
959 // When the terrain updates, predraw a black space around its edge
960 // to keep various former interface elements from showing through the gaps
961
962 //calculate width differences between map_font and font
963 int partial_width = std::max( TERRAIN_WINDOW_TERM_WIDTH * fontwidth - TERRAIN_WINDOW_WIDTH *
964 map_font->width, 0 );
965 int partial_height = std::max( TERRAIN_WINDOW_TERM_HEIGHT * fontheight - TERRAIN_WINDOW_HEIGHT *
966 map_font->height, 0 );
967 //Gap between terrain and lower window edge
968 if( partial_height > 0 ) {
969 geometry->rect( renderer, point( win->pos.x * map_font->width,
970 ( win->pos.y + TERRAIN_WINDOW_HEIGHT ) * map_font->height ),
971 TERRAIN_WINDOW_WIDTH * map_font->width + partial_width, partial_height,
972 color_as_sdl( catacurses::black ) );
973 }
974 //Gap between terrain and sidebar
975 if( partial_width > 0 ) {
976 geometry->rect( renderer, point( ( win->pos.x + TERRAIN_WINDOW_WIDTH ) * map_font->width,
977 win->pos.y * map_font->height ),
978 partial_width,
979 TERRAIN_WINDOW_HEIGHT * map_font->height + partial_height,
980 color_as_sdl( catacurses::black ) );
981 }
982 // Special font for the terrain window
983 update = draw_window( map_font, w );
984 } else if( g && w == g->w_overmap && overmap_font ) {
985 // Special font for the terrain window
986 update = draw_window( overmap_font, w );
987 } else if( g && w == g->w_pixel_minimap && pixel_minimap_option ) {
988 // ensure the space the minimap covers is "dirtied".
989 // this is necessary when it's the only part of the sidebar being drawn
990 // TODO: Figure out how to properly make the minimap code do whatever it is this does
991 draw_window( font, w );
992
993 // Make sure the entire minimap window is black before drawing.
994 clear_window_area( w );
995 tilecontext->draw_minimap(
996 point( win->pos.x * fontwidth, win->pos.y * fontheight ),
997 tripoint( get_player_character().pos().xy(), g->ter_view_p.z ),
998 win->width * font->width, win->height * font->height );
999 update = true;
1000
1001 } else {
1002 // Either not using tiles (tilecontext) or not the w_terrain window.
1003 update = draw_window( font, w );
1004 }
1005 if( update ) {
1006 needupdate = true;
1007 }
1008 }
1009
1010 static int alt_buffer = 0;
1011 static bool alt_down = false;
1012
begin_alt_code()1013 static void begin_alt_code()
1014 {
1015 alt_buffer = 0;
1016 alt_down = true;
1017 }
1018
add_alt_code(char c)1019 static bool add_alt_code( char c )
1020 {
1021 if( alt_down && c >= '0' && c <= '9' ) {
1022 alt_buffer = alt_buffer * 10 + ( c - '0' );
1023 return true;
1024 }
1025 return false;
1026 }
1027
end_alt_code()1028 static int end_alt_code()
1029 {
1030 alt_down = false;
1031 return alt_buffer;
1032 }
1033
HandleDPad()1034 static int HandleDPad()
1035 {
1036 // Check if we have a gamepad d-pad event.
1037 if( SDL_JoystickGetHat( joystick, 0 ) != SDL_HAT_CENTERED ) {
1038 // When someone tries to press a diagonal, they likely will
1039 // press a single direction first. Wait a few milliseconds to
1040 // give them time to press both of the buttons for the diagonal.
1041 int button = SDL_JoystickGetHat( joystick, 0 );
1042 int lc = ERR;
1043 if( button == SDL_HAT_LEFT ) {
1044 lc = JOY_LEFT;
1045 } else if( button == SDL_HAT_DOWN ) {
1046 lc = JOY_DOWN;
1047 } else if( button == SDL_HAT_RIGHT ) {
1048 lc = JOY_RIGHT;
1049 } else if( button == SDL_HAT_UP ) {
1050 lc = JOY_UP;
1051 } else if( button == SDL_HAT_LEFTUP ) {
1052 lc = JOY_LEFTUP;
1053 } else if( button == SDL_HAT_LEFTDOWN ) {
1054 lc = JOY_LEFTDOWN;
1055 } else if( button == SDL_HAT_RIGHTUP ) {
1056 lc = JOY_RIGHTUP;
1057 } else if( button == SDL_HAT_RIGHTDOWN ) {
1058 lc = JOY_RIGHTDOWN;
1059 }
1060
1061 if( delaydpad == std::numeric_limits<Uint32>::max() ) {
1062 delaydpad = SDL_GetTicks() + dpad_delay;
1063 queued_dpad = lc;
1064 }
1065
1066 // Okay it seems we're ready to process.
1067 if( SDL_GetTicks() > delaydpad ) {
1068
1069 if( lc != ERR ) {
1070 if( dpad_continuous && ( lc & lastdpad ) == 0 ) {
1071 // Continuous movement should only work in the same or similar directions.
1072 dpad_continuous = false;
1073 lastdpad = lc;
1074 return 0;
1075 }
1076
1077 last_input = input_event( lc, input_event_t::gamepad );
1078 lastdpad = lc;
1079 queued_dpad = ERR;
1080
1081 if( !dpad_continuous ) {
1082 delaydpad = SDL_GetTicks() + 200;
1083 dpad_continuous = true;
1084 } else {
1085 delaydpad = SDL_GetTicks() + 60;
1086 }
1087 return 1;
1088 }
1089 }
1090 } else {
1091 dpad_continuous = false;
1092 delaydpad = std::numeric_limits<Uint32>::max();
1093
1094 // If we didn't hold it down for a while, just
1095 // fire the last registered press.
1096 if( queued_dpad != ERR ) {
1097 last_input = input_event( queued_dpad, input_event_t::gamepad );
1098 queued_dpad = ERR;
1099 return 1;
1100 }
1101 }
1102
1103 return 0;
1104 }
1105
sdl_keycode_opposite_arrow(SDL_Keycode key)1106 static SDL_Keycode sdl_keycode_opposite_arrow( SDL_Keycode key )
1107 {
1108 switch( key ) {
1109 case SDLK_UP:
1110 return SDLK_DOWN;
1111 case SDLK_DOWN:
1112 return SDLK_UP;
1113 case SDLK_LEFT:
1114 return SDLK_RIGHT;
1115 case SDLK_RIGHT:
1116 return SDLK_LEFT;
1117 }
1118 return 0;
1119 }
1120
sdl_keycode_is_arrow(SDL_Keycode key)1121 static bool sdl_keycode_is_arrow( SDL_Keycode key )
1122 {
1123 return static_cast<bool>( sdl_keycode_opposite_arrow( key ) );
1124 }
1125
arrow_combo_to_numpad(SDL_Keycode mod,SDL_Keycode key)1126 static int arrow_combo_to_numpad( SDL_Keycode mod, SDL_Keycode key )
1127 {
1128 if( ( mod == SDLK_UP && key == SDLK_RIGHT ) ||
1129 ( mod == SDLK_RIGHT && key == SDLK_UP ) ) {
1130 return KEY_NUM( 9 );
1131 }
1132 if( ( mod == SDLK_UP && key == SDLK_UP ) ) {
1133 return KEY_NUM( 8 );
1134 }
1135 if( ( mod == SDLK_UP && key == SDLK_LEFT ) ||
1136 ( mod == SDLK_LEFT && key == SDLK_UP ) ) {
1137 return KEY_NUM( 7 );
1138 }
1139 if( ( mod == SDLK_RIGHT && key == SDLK_RIGHT ) ) {
1140 return KEY_NUM( 6 );
1141 }
1142 if( mod == sdl_keycode_opposite_arrow( key ) ) {
1143 return KEY_NUM( 5 );
1144 }
1145 if( ( mod == SDLK_LEFT && key == SDLK_LEFT ) ) {
1146 return KEY_NUM( 4 );
1147 }
1148 if( ( mod == SDLK_DOWN && key == SDLK_RIGHT ) ||
1149 ( mod == SDLK_RIGHT && key == SDLK_DOWN ) ) {
1150 return KEY_NUM( 3 );
1151 }
1152 if( ( mod == SDLK_DOWN && key == SDLK_DOWN ) ) {
1153 return KEY_NUM( 2 );
1154 }
1155 if( ( mod == SDLK_DOWN && key == SDLK_LEFT ) ||
1156 ( mod == SDLK_LEFT && key == SDLK_DOWN ) ) {
1157 return KEY_NUM( 1 );
1158 }
1159 return 0;
1160 }
1161
1162 static int arrow_combo_modifier = 0;
1163
handle_arrow_combo(SDL_Keycode key)1164 static int handle_arrow_combo( SDL_Keycode key )
1165 {
1166 if( !arrow_combo_modifier ) {
1167 arrow_combo_modifier = key;
1168 return 0;
1169 }
1170 return arrow_combo_to_numpad( arrow_combo_modifier, key );
1171 }
1172
end_arrow_combo()1173 static void end_arrow_combo()
1174 {
1175 arrow_combo_modifier = 0;
1176 }
1177
1178 /**
1179 * Translate SDL key codes to key identifiers used by ncurses, this
1180 * allows the input_manager to only consider those.
1181 * @return 0 if the input can not be translated (unknown key?),
1182 * -1 when a ALT+number sequence has been started,
1183 * or something that a call to ncurses getch would return.
1184 */
sdl_keysym_to_curses(const SDL_Keysym & keysym)1185 static int sdl_keysym_to_curses( const SDL_Keysym &keysym )
1186 {
1187
1188 const std::string diag_mode = get_option<std::string>( "DIAG_MOVE_WITH_MODIFIERS_MODE" );
1189
1190 if( diag_mode == "mode1" ) {
1191 if( keysym.mod & KMOD_CTRL && sdl_keycode_is_arrow( keysym.sym ) ) {
1192 return handle_arrow_combo( keysym.sym );
1193 } else {
1194 end_arrow_combo();
1195 }
1196 }
1197
1198 if( diag_mode == "mode2" ) {
1199 //Shift + Cursor Arrow (diagonal clockwise)
1200 if( keysym.mod & KMOD_SHIFT ) {
1201 switch( keysym.sym ) {
1202 case SDLK_LEFT:
1203 return inp_mngr.get_first_char_for_action( "LEFTUP" );
1204 case SDLK_RIGHT:
1205 return inp_mngr.get_first_char_for_action( "RIGHTDOWN" );
1206 case SDLK_UP:
1207 return inp_mngr.get_first_char_for_action( "RIGHTUP" );
1208 case SDLK_DOWN:
1209 return inp_mngr.get_first_char_for_action( "LEFTDOWN" );
1210 }
1211 }
1212 //Ctrl + Cursor Arrow (diagonal counter-clockwise)
1213 if( keysym.mod & KMOD_CTRL ) {
1214 switch( keysym.sym ) {
1215 case SDLK_LEFT:
1216 return inp_mngr.get_first_char_for_action( "LEFTDOWN" );
1217 case SDLK_RIGHT:
1218 return inp_mngr.get_first_char_for_action( "RIGHTUP" );
1219 case SDLK_UP:
1220 return inp_mngr.get_first_char_for_action( "LEFTUP" );
1221 case SDLK_DOWN:
1222 return inp_mngr.get_first_char_for_action( "RIGHTDOWN" );
1223 }
1224 }
1225 }
1226
1227 if( diag_mode == "mode3" ) {
1228 //Shift + Cursor Left/RightArrow
1229 if( keysym.mod & KMOD_SHIFT ) {
1230 switch( keysym.sym ) {
1231 case SDLK_LEFT:
1232 return inp_mngr.get_first_char_for_action( "LEFTUP" );
1233 case SDLK_RIGHT:
1234 return inp_mngr.get_first_char_for_action( "RIGHTUP" );
1235 }
1236 }
1237 //Ctrl + Cursor Left/Right Arrow
1238 if( keysym.mod & KMOD_CTRL ) {
1239 switch( keysym.sym ) {
1240 case SDLK_LEFT:
1241 return inp_mngr.get_first_char_for_action( "LEFTDOWN" );
1242 case SDLK_RIGHT:
1243 return inp_mngr.get_first_char_for_action( "RIGHTDOWN" );
1244 }
1245 }
1246 }
1247
1248 if( keysym.mod & KMOD_CTRL && keysym.sym >= 'a' && keysym.sym <= 'z' ) {
1249 // ASCII ctrl codes, ^A through ^Z.
1250 return keysym.sym - 'a' + '\1';
1251 }
1252 switch( keysym.sym ) {
1253 // This is special: allow entering a Unicode character with ALT+number
1254 case SDLK_RALT:
1255 case SDLK_LALT:
1256 begin_alt_code();
1257 return -1;
1258 // The following are simple translations:
1259 case SDLK_KP_ENTER:
1260 case SDLK_RETURN:
1261 case SDLK_RETURN2:
1262 return '\n';
1263 case SDLK_BACKSPACE:
1264 case SDLK_KP_BACKSPACE:
1265 return KEY_BACKSPACE;
1266 case SDLK_DELETE:
1267 return KEY_DC;
1268 case SDLK_ESCAPE:
1269 return KEY_ESCAPE;
1270 case SDLK_TAB:
1271 if( keysym.mod & KMOD_SHIFT ) {
1272 return KEY_BTAB;
1273 }
1274 return '\t';
1275 case SDLK_LEFT:
1276 return KEY_LEFT;
1277 case SDLK_RIGHT:
1278 return KEY_RIGHT;
1279 case SDLK_UP:
1280 return KEY_UP;
1281 case SDLK_DOWN:
1282 return KEY_DOWN;
1283 case SDLK_PAGEUP:
1284 return KEY_PPAGE;
1285 case SDLK_PAGEDOWN:
1286 return KEY_NPAGE;
1287 case SDLK_HOME:
1288 return KEY_HOME;
1289 case SDLK_END:
1290 return KEY_END;
1291 case SDLK_F1:
1292 return KEY_F( 1 );
1293 case SDLK_F2:
1294 return KEY_F( 2 );
1295 case SDLK_F3:
1296 return KEY_F( 3 );
1297 case SDLK_F4:
1298 return KEY_F( 4 );
1299 case SDLK_F5:
1300 return KEY_F( 5 );
1301 case SDLK_F6:
1302 return KEY_F( 6 );
1303 case SDLK_F7:
1304 return KEY_F( 7 );
1305 case SDLK_F8:
1306 return KEY_F( 8 );
1307 case SDLK_F9:
1308 return KEY_F( 9 );
1309 case SDLK_F10:
1310 return KEY_F( 10 );
1311 case SDLK_F11:
1312 return KEY_F( 11 );
1313 case SDLK_F12:
1314 return KEY_F( 12 );
1315 case SDLK_F13:
1316 return KEY_F( 13 );
1317 case SDLK_F14:
1318 return KEY_F( 14 );
1319 case SDLK_F15:
1320 return KEY_F( 15 );
1321 // Every other key is ignored as there is no curses constant for it.
1322 // TODO: add more if you find more.
1323 default:
1324 return 0;
1325 }
1326 }
1327
sdl_keysym_to_keycode_evt(const SDL_Keysym & keysym)1328 static input_event sdl_keysym_to_keycode_evt( const SDL_Keysym &keysym )
1329 {
1330 switch( keysym.sym ) {
1331 case SDLK_LCTRL:
1332 case SDLK_LSHIFT:
1333 case SDLK_LALT:
1334 case SDLK_RCTRL:
1335 case SDLK_RSHIFT:
1336 case SDLK_RALT:
1337 return input_event();
1338 }
1339 input_event evt;
1340 evt.type = input_event_t::keyboard_code;
1341 if( keysym.mod & KMOD_CTRL ) {
1342 evt.modifiers.emplace( keymod_t::ctrl );
1343 }
1344 if( keysym.mod & KMOD_ALT ) {
1345 evt.modifiers.emplace( keymod_t::alt );
1346 }
1347 if( keysym.mod & KMOD_SHIFT ) {
1348 evt.modifiers.emplace( keymod_t::shift );
1349 }
1350 evt.sequence.emplace_back( keysym.sym );
1351 return evt;
1352 }
1353
handle_resize(int w,int h)1354 bool handle_resize( int w, int h )
1355 {
1356 if( ( w != WindowWidth ) || ( h != WindowHeight ) ) {
1357 WindowWidth = w;
1358 WindowHeight = h;
1359 TERMINAL_WIDTH = WindowWidth / fontwidth / scaling_factor;
1360 TERMINAL_HEIGHT = WindowHeight / fontheight / scaling_factor;
1361 need_invalidate_framebuffers = true;
1362 catacurses::stdscr = catacurses::newwin( TERMINAL_HEIGHT, TERMINAL_WIDTH, point_zero );
1363 SetupRenderTarget();
1364 game_ui::init_ui();
1365 ui_manager::screen_resized();
1366 return true;
1367 }
1368 return false;
1369 }
1370
resize_term(const int cell_w,const int cell_h)1371 void resize_term( const int cell_w, const int cell_h )
1372 {
1373 int w = cell_w * fontwidth * scaling_factor;
1374 int h = cell_h * fontheight * scaling_factor;
1375 SDL_SetWindowSize( window.get(), w, h );
1376 SDL_GetWindowSize( window.get(), &w, &h );
1377 handle_resize( w, h );
1378 }
1379
toggle_fullscreen_window()1380 void toggle_fullscreen_window()
1381 {
1382 static int restore_win_w = get_option<int>( "TERMINAL_X" ) * fontwidth * scaling_factor;
1383 static int restore_win_h = get_option<int>( "TERMINAL_Y" ) * fontheight * scaling_factor;
1384
1385 if( fullscreen ) {
1386 if( printErrorIf( SDL_SetWindowFullscreen( window.get(), 0 ) != 0,
1387 "SDL_SetWindowFullscreen failed" ) ) {
1388 return;
1389 }
1390 SDL_RestoreWindow( window.get() );
1391 SDL_SetWindowSize( window.get(), restore_win_w, restore_win_h );
1392 SDL_SetWindowMinimumSize( window.get(), fontwidth * FULL_SCREEN_WIDTH * scaling_factor,
1393 fontheight * FULL_SCREEN_HEIGHT * scaling_factor );
1394 } else {
1395 restore_win_w = WindowWidth;
1396 restore_win_h = WindowHeight;
1397 if( printErrorIf( SDL_SetWindowFullscreen( window.get(), SDL_WINDOW_FULLSCREEN_DESKTOP ) != 0,
1398 "SDL_SetWindowFullscreen failed" ) ) {
1399 return;
1400 }
1401 }
1402 int nw = 0;
1403 int nh = 0;
1404 SDL_GetWindowSize( window.get(), &nw, &nh );
1405 handle_resize( nw, nh );
1406 fullscreen = !fullscreen;
1407 }
1408
1409 #if defined(__ANDROID__)
1410 static float finger_down_x = -1.0f; // in pixels
1411 static float finger_down_y = -1.0f; // in pixels
1412 static float finger_curr_x = -1.0f; // in pixels
1413 static float finger_curr_y = -1.0f; // in pixels
1414 static float second_finger_down_x = -1.0f; // in pixels
1415 static float second_finger_down_y = -1.0f; // in pixels
1416 static float second_finger_curr_x = -1.0f; // in pixels
1417 static float second_finger_curr_y = -1.0f; // in pixels
1418 // when did the first finger start touching the screen? 0 if not touching, otherwise the time in milliseconds.
1419 static uint32_t finger_down_time = 0;
1420 // the last time we repeated input for a finger hold, 0 if not touching, otherwise the time in milliseconds.
1421 static uint32_t finger_repeat_time = 0;
1422 // the last time a single tap was detected. used for double-tap detection.
1423 static uint32_t last_tap_time = 0;
1424 // when did the hardware back button start being pressed? 0 if not touching, otherwise the time in milliseconds.
1425 static uint32_t ac_back_down_time = 0;
1426 // has a second finger touched the screen while the first was touching?
1427 static bool is_two_finger_touch = false;
1428 // did this touch start on a quick shortcut?
1429 static bool is_quick_shortcut_touch = false;
1430 static bool quick_shortcuts_toggle_handled = false;
1431 // the current finger repeat delay - will be somewhere between the min/max values depending on user input
1432 uint32_t finger_repeat_delay = 500;
1433 // should we make sure the sdl surface is visible? set to true whenever the SDL window is shown.
1434 static bool needs_sdl_surface_visibility_refresh = true;
1435
1436 // Quick shortcuts container: maps the touch input context category (std::string) to a std::list of input_events.
1437 using quick_shortcuts_t = std::list<input_event>;
1438 std::map<std::string, quick_shortcuts_t> quick_shortcuts_map;
1439
1440 // A copy of the last known input_context from the input manager. It's important this is a copy, as there are times
1441 // the input manager has an empty input_context (eg. when player is moving over slow objects) and we don't want our
1442 // quick shortcuts to disappear momentarily.
1443 input_context touch_input_context;
1444
get_quick_shortcut_name(const std::string & category)1445 std::string get_quick_shortcut_name( const std::string &category )
1446 {
1447 if( category == "DEFAULTMODE" &&
1448 g->check_zone( zone_type_id( "NO_AUTO_PICKUP" ), get_player_character().pos() ) &&
1449 get_option<bool>( "ANDROID_SHORTCUT_ZONE" ) ) {
1450 return "DEFAULTMODE____SHORTCUTS";
1451 }
1452 return category;
1453 }
1454
android_get_display_density()1455 float android_get_display_density()
1456 {
1457 JNIEnv *env = ( JNIEnv * )SDL_AndroidGetJNIEnv();
1458 jobject activity = ( jobject )SDL_AndroidGetActivity();
1459 jclass clazz( env->GetObjectClass( activity ) );
1460 jmethodID method_id = env->GetMethodID( clazz, "getDisplayDensity", "()F" );
1461 jfloat ans = env->CallFloatMethod( activity, method_id );
1462 env->DeleteLocalRef( activity );
1463 env->DeleteLocalRef( clazz );
1464 return ans;
1465 }
1466
1467 // given the active quick shortcuts, returns the dimensions of each quick shortcut button.
get_quick_shortcut_dimensions(quick_shortcuts_t & qsl,float & border,float & width,float & height)1468 void get_quick_shortcut_dimensions( quick_shortcuts_t &qsl, float &border, float &width,
1469 float &height )
1470 {
1471 const float shortcut_dimensions_authored_density = 3.0f; // 480p xxhdpi
1472 float screen_density_scale = android_get_display_density() / shortcut_dimensions_authored_density;
1473 border = std::floor( screen_density_scale * get_option<int>( "ANDROID_SHORTCUT_BORDER" ) );
1474 width = std::floor( screen_density_scale * get_option<int>( "ANDROID_SHORTCUT_WIDTH_MAX" ) );
1475 float min_width = std::floor( screen_density_scale * std::min(
1476 get_option<int>( "ANDROID_SHORTCUT_WIDTH_MIN" ),
1477 get_option<int>( "ANDROID_SHORTCUT_WIDTH_MAX" ) ) );
1478 float usable_window_width = WindowWidth * get_option<int>( "ANDROID_SHORTCUT_SCREEN_PERCENTAGE" ) *
1479 0.01f;
1480 if( width * qsl.size() > usable_window_width ) {
1481 width *= usable_window_width / ( width * qsl.size() );
1482 if( width < min_width ) {
1483 width = min_width;
1484 }
1485 }
1486 width = std::floor( width );
1487 height = std::floor( screen_density_scale * get_option<int>( "ANDROID_SHORTCUT_HEIGHT" ) );
1488 }
1489
1490 // Returns the quick shortcut (if any) under the finger's current position, or finger down position if down == true
get_quick_shortcut_under_finger(bool down=false)1491 input_event *get_quick_shortcut_under_finger( bool down = false )
1492 {
1493
1494 if( !quick_shortcuts_enabled ) {
1495 return NULL;
1496 }
1497
1498 quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name(
1499 touch_input_context.get_category() )];
1500
1501 float border, width, height;
1502 get_quick_shortcut_dimensions( qsl, border, width, height );
1503
1504 float finger_y = down ? finger_down_y : finger_curr_y;
1505 if( finger_y < WindowHeight - height ) {
1506 return NULL;
1507 }
1508
1509 int i = 0;
1510 bool shortcut_right = get_option<std::string>( "ANDROID_SHORTCUT_POSITION" ) == "right";
1511 float finger_x = down ? finger_down_x : finger_curr_x;
1512 for( std::list<input_event>::iterator it = qsl.begin(); it != qsl.end(); ++it ) {
1513 if( ( i + 1 ) * width > WindowWidth * get_option<int>( "ANDROID_SHORTCUT_SCREEN_PERCENTAGE" ) *
1514 0.01f ) {
1515 continue;
1516 }
1517 i++;
1518 if( shortcut_right ) {
1519 if( finger_x > WindowWidth - ( i * width ) ) {
1520 return &( *it );
1521 }
1522 } else {
1523 if( finger_x < i * width ) {
1524 return &( *it );
1525 }
1526 }
1527 }
1528
1529 return NULL;
1530 }
1531
1532 // when pre-populating a quick shortcut list with defaults, ignore these actions (since they're all handleable by native touch operations)
ignore_action_for_quick_shortcuts(const std::string & action)1533 bool ignore_action_for_quick_shortcuts( const std::string &action )
1534 {
1535 return ( action == "UP"
1536 || action == "DOWN"
1537 || action == "LEFT"
1538 || action == "RIGHT"
1539 || action == "LEFTUP"
1540 || action == "LEFTDOWN"
1541 || action == "RIGHTUP"
1542 || action == "RIGHTDOWN"
1543 || action == "QUIT"
1544 || action == "CONFIRM"
1545 || action == "MOVE_SINGLE_ITEM" // maps to ENTER
1546 || action == "MOVE_ARMOR" // maps to ENTER
1547 || action == "ANY_INPUT"
1548 || action ==
1549 "DELETE_TEMPLATE" // strictly we shouldn't have this one, but I don't like seeing the "d" on the main menu by default. :)
1550 );
1551 }
1552
1553 // Adds a quick shortcut to a quick_shortcut list, setting shortcut_last_used_action_counter accordingly.
add_quick_shortcut(quick_shortcuts_t & qsl,input_event & event,bool back,bool reset_shortcut_last_used_action_counter)1554 void add_quick_shortcut( quick_shortcuts_t &qsl, input_event &event, bool back,
1555 bool reset_shortcut_last_used_action_counter )
1556 {
1557 if( reset_shortcut_last_used_action_counter && g ) {
1558 event.shortcut_last_used_action_counter =
1559 g->get_user_action_counter(); // only used for DEFAULTMODE
1560 }
1561 if( back ) {
1562 qsl.push_back( event );
1563 } else {
1564 qsl.push_front( event );
1565 }
1566 }
1567
1568 // Given a quick shortcut list and a specific key, move that key to the front or back of the list.
reorder_quick_shortcut(quick_shortcuts_t & qsl,int key,bool back)1569 void reorder_quick_shortcut( quick_shortcuts_t &qsl, int key, bool back )
1570 {
1571 for( const auto &event : qsl ) {
1572 if( event.get_first_input() == key ) {
1573 input_event event_copy = event;
1574 qsl.remove( event );
1575 add_quick_shortcut( qsl, event_copy, back, false );
1576 break;
1577 }
1578 }
1579 }
1580
reorder_quick_shortcuts(quick_shortcuts_t & qsl)1581 void reorder_quick_shortcuts( quick_shortcuts_t &qsl )
1582 {
1583 // Do some manual reordering to make transitions between input contexts more consistent
1584 // Desired order of keys: < > BACKTAB TAB PPAGE NPAGE . . . . ?
1585 bool shortcut_right = get_option<std::string>( "ANDROID_SHORTCUT_POSITION" ) == "right";
1586 if( shortcut_right ) {
1587 reorder_quick_shortcut( qsl, KEY_PPAGE, false ); // paging control
1588 reorder_quick_shortcut( qsl, KEY_NPAGE, false );
1589 reorder_quick_shortcut( qsl, KEY_BTAB, false ); // secondary tabs after that
1590 reorder_quick_shortcut( qsl, '\t', false );
1591 reorder_quick_shortcut( qsl, '<', false ); // tabs next
1592 reorder_quick_shortcut( qsl, '>', false );
1593 reorder_quick_shortcut( qsl, '?', false ); // help at the start
1594 } else {
1595 reorder_quick_shortcut( qsl, KEY_NPAGE, false );
1596 reorder_quick_shortcut( qsl, KEY_PPAGE, false ); // paging control
1597 reorder_quick_shortcut( qsl, '\t', false );
1598 reorder_quick_shortcut( qsl, KEY_BTAB, false ); // secondary tabs after that
1599 reorder_quick_shortcut( qsl, '>', false );
1600 reorder_quick_shortcut( qsl, '<', false ); // tabs next
1601 reorder_quick_shortcut( qsl, '?', false ); // help at the start
1602 }
1603 }
1604
choose_best_key_for_action(const std::string & action,const std::string & category)1605 int choose_best_key_for_action( const std::string &action, const std::string &category )
1606 {
1607 const std::vector<input_event> &events = inp_mngr.get_input_for_action( action, category );
1608 int best_key = -1;
1609 for( const auto &events_event : events ) {
1610 if( events_event.type == input_event_t::keyboard_char && events_event.sequence.size() == 1 ) {
1611 bool is_ascii_char = isprint( events_event.sequence.front() ) &&
1612 events_event.sequence.front() < 0xFF;
1613 bool is_best_ascii_char = best_key >= 0 && isprint( best_key ) && best_key < 0xFF;
1614 if( best_key < 0 || ( is_ascii_char && !is_best_ascii_char ) ) {
1615 best_key = events_event.sequence.front();
1616 }
1617 }
1618 }
1619 return best_key;
1620 }
1621
add_key_to_quick_shortcuts(int key,const std::string & category,bool back)1622 bool add_key_to_quick_shortcuts( int key, const std::string &category, bool back )
1623 {
1624 if( key > 0 ) {
1625 quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name( category )];
1626 input_event event = input_event( key, input_event_t::keyboard_char );
1627 quick_shortcuts_t::iterator it = std::find( qsl.begin(), qsl.end(), event );
1628 if( it != qsl.end() ) { // already exists
1629 ( *it ).shortcut_last_used_action_counter =
1630 g->get_user_action_counter(); // make sure we refresh shortcut usage
1631 } else {
1632 add_quick_shortcut( qsl, event, back,
1633 true ); // doesn't exist, add it to the shortcuts and refresh shortcut usage
1634 return true;
1635 }
1636 }
1637 return false;
1638 }
add_best_key_for_action_to_quick_shortcuts(std::string action_str,const std::string & category,bool back)1639 bool add_best_key_for_action_to_quick_shortcuts( std::string action_str,
1640 const std::string &category, bool back )
1641 {
1642 int best_key = choose_best_key_for_action( action_str, category );
1643 return add_key_to_quick_shortcuts( best_key, category, back );
1644 }
1645
add_best_key_for_action_to_quick_shortcuts(action_id action,const std::string & category,bool back)1646 bool add_best_key_for_action_to_quick_shortcuts( action_id action, const std::string &category,
1647 bool back )
1648 {
1649 return add_best_key_for_action_to_quick_shortcuts( action_ident( action ), category, back );
1650 }
1651
remove_action_from_quick_shortcuts(std::string action_str,const std::string & category)1652 void remove_action_from_quick_shortcuts( std::string action_str, const std::string &category )
1653 {
1654 quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name( category )];
1655 const std::vector<input_event> &events = inp_mngr.get_input_for_action( action_str, category );
1656 for( const auto &event : events ) {
1657 qsl.remove( event );
1658 }
1659 }
1660
remove_action_from_quick_shortcuts(action_id action,const std::string & category)1661 void remove_action_from_quick_shortcuts( action_id action, const std::string &category )
1662 {
1663 remove_action_from_quick_shortcuts( action_ident( action ), category );
1664 }
1665
1666 // Returns true if an expired action was removed
remove_expired_actions_from_quick_shortcuts(const std::string & category)1667 bool remove_expired_actions_from_quick_shortcuts( const std::string &category )
1668 {
1669 int remove_turns = get_option<int>( "ANDROID_SHORTCUT_REMOVE_TURNS" );
1670 if( remove_turns <= 0 ) {
1671 return false;
1672 }
1673
1674 // This should only ever be used on "DEFAULTMODE" category for gameplay shortcuts
1675 if( category != "DEFAULTMODE" ) {
1676 return false;
1677 }
1678
1679 bool ret = false;
1680 quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name( category )];
1681 quick_shortcuts_t::iterator it = qsl.begin();
1682 while( it != qsl.end() ) {
1683 if( g->get_user_action_counter() - ( *it ).shortcut_last_used_action_counter > remove_turns ) {
1684 it = qsl.erase( it );
1685 ret = true;
1686 } else {
1687 ++it;
1688 }
1689 }
1690 return ret;
1691 }
1692
remove_stale_inventory_quick_shortcuts()1693 void remove_stale_inventory_quick_shortcuts()
1694 {
1695 if( get_option<bool>( "ANDROID_INVENTORY_AUTOADD" ) ) {
1696 quick_shortcuts_t &qsl = quick_shortcuts_map["INVENTORY"];
1697 quick_shortcuts_t::iterator it = qsl.begin();
1698 bool in_inventory;
1699 int key;
1700 bool valid;
1701 while( it != qsl.end() ) {
1702 key = ( *it ).get_first_input();
1703 valid = inv_chars.valid( key );
1704 in_inventory = false;
1705 if( valid ) {
1706 Character &player_character = get_player_character();
1707 in_inventory = player_character.inv->invlet_to_position( key ) != INT_MIN;
1708 if( !in_inventory ) {
1709 // We couldn't find this item in the inventory, let's check worn items
1710 for( const auto &item : player_character.worn ) {
1711 if( item.invlet == key ) {
1712 in_inventory = true;
1713 break;
1714 }
1715 }
1716 }
1717 if( !in_inventory ) {
1718 // We couldn't find it in worn items either, check weapon held
1719 if( player_character.weapon.invlet == key ) {
1720 in_inventory = true;
1721 }
1722 }
1723 }
1724 if( valid && !in_inventory ) {
1725 it = qsl.erase( it );
1726 } else {
1727 ++it;
1728 }
1729 }
1730 }
1731 }
1732
1733 // Draw preview of terminal size when adjusting values
draw_terminal_size_preview()1734 void draw_terminal_size_preview()
1735 {
1736 bool preview_terminal_dirty = preview_terminal_width != get_option<int>( "TERMINAL_X" ) * fontwidth
1737 ||
1738 preview_terminal_height != get_option<int>( "TERMINAL_Y" ) * fontheight;
1739 if( preview_terminal_dirty ||
1740 ( preview_terminal_change_time > 0 && SDL_GetTicks() - preview_terminal_change_time < 1000 ) ) {
1741 if( preview_terminal_dirty ) {
1742 preview_terminal_width = get_option<int>( "TERMINAL_X" ) * fontwidth;
1743 preview_terminal_height = get_option<int>( "TERMINAL_Y" ) * fontheight;
1744 preview_terminal_change_time = SDL_GetTicks();
1745 }
1746 SetRenderDrawColor( renderer, 255, 255, 255, 255 );
1747 SDL_Rect previewrect = get_android_render_rect( preview_terminal_width, preview_terminal_height );
1748 SDL_RenderDrawRect( renderer.get(), &previewrect );
1749 SetRenderDrawColor( renderer, 0, 0, 0, 255 );
1750 }
1751 }
1752
1753 // Draw quick shortcuts on top of the game view
draw_quick_shortcuts()1754 void draw_quick_shortcuts()
1755 {
1756
1757 if( !quick_shortcuts_enabled ||
1758 SDL_IsTextInputActive() ||
1759 ( get_option<bool>( "ANDROID_HIDE_HOLDS" ) && !is_quick_shortcut_touch && finger_down_time > 0 &&
1760 SDL_GetTicks() - finger_down_time >= static_cast<uint32_t>(
1761 get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) ) { // player is swipe + holding in a direction
1762 return;
1763 }
1764
1765 bool shortcut_right = get_option<std::string>( "ANDROID_SHORTCUT_POSITION" ) == "right";
1766 std::string &category = touch_input_context.get_category();
1767 bool is_default_mode = category == "DEFAULTMODE";
1768 quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name( category )];
1769 if( qsl.empty() || !touch_input_context.get_registered_manual_keys().empty() ) {
1770 if( category == "DEFAULTMODE" ) {
1771 const std::string default_gameplay_shortcuts =
1772 get_option<std::string>( "ANDROID_SHORTCUT_DEFAULTS" );
1773 for( const auto &c : default_gameplay_shortcuts ) {
1774 add_key_to_quick_shortcuts( c, category, true );
1775 }
1776 } else {
1777 // This is an empty quick-shortcuts list, let's pre-populate it as best we can from the input context
1778
1779 // For manual key lists, force-clear them each time since there's no point allowing custom bindings anyway
1780 if( !touch_input_context.get_registered_manual_keys().empty() ) {
1781 qsl.clear();
1782 }
1783
1784 // First process registered actions
1785 std::vector<std::string> ®istered_actions = touch_input_context.get_registered_actions();
1786 for( std::vector<std::string>::iterator it = registered_actions.begin();
1787 it != registered_actions.end(); ++it ) {
1788 std::string &action = *it;
1789 if( ignore_action_for_quick_shortcuts( action ) ) {
1790 continue;
1791 }
1792
1793 add_best_key_for_action_to_quick_shortcuts( action, category, !shortcut_right );
1794 }
1795
1796 // Then process manual keys
1797 std::vector<input_context::manual_key> ®istered_manual_keys =
1798 touch_input_context.get_registered_manual_keys();
1799 for( const auto &manual_key : registered_manual_keys ) {
1800 input_event event( manual_key.key, input_event_t::keyboard_char );
1801 add_quick_shortcut( qsl, event, !shortcut_right, true );
1802 }
1803 }
1804 }
1805
1806 // Only reorder quick shortcuts for non-gameplay lists that are likely to have navigational menu stuff
1807 if( !is_default_mode ) {
1808 reorder_quick_shortcuts( qsl );
1809 }
1810
1811 float border, width, height;
1812 get_quick_shortcut_dimensions( qsl, border, width, height );
1813 input_event *hovered_quick_shortcut = get_quick_shortcut_under_finger();
1814 SDL_Rect rect;
1815 bool hovered, show_hint;
1816 int i = 0;
1817 for( std::list<input_event>::iterator it = qsl.begin(); it != qsl.end(); ++it ) {
1818 if( ( i + 1 ) * width > WindowWidth * get_option<int>( "ANDROID_SHORTCUT_SCREEN_PERCENTAGE" ) *
1819 0.01f ) {
1820 continue;
1821 }
1822 input_event &event = *it;
1823 std::string text = event.text;
1824 int key = event.get_first_input();
1825 float default_text_scale = std::floor( 0.75f * ( height /
1826 font->height ) ); // default for single character strings
1827 float text_scale = default_text_scale;
1828 if( text.empty() || text == " " ) {
1829 text = inp_mngr.get_keyname( key, event.type );
1830 text_scale = std::min( text_scale, 0.75f * ( width / ( font->width * utf8_width( text ) ) ) );
1831 }
1832 hovered = is_quick_shortcut_touch && hovered_quick_shortcut == &event;
1833 show_hint = hovered &&
1834 SDL_GetTicks() - finger_down_time > static_cast<uint32_t>
1835 ( get_option<int>( "ANDROID_INITIAL_DELAY" ) );
1836 std::string hint_text;
1837 if( show_hint ) {
1838 if( touch_input_context.get_category() == "INVENTORY" && inv_chars.valid( key ) ) {
1839 Character &player_character = get_player_character();
1840 // Special case for inventory items - show the inventory item name as help text
1841 hint_text = player_character.inv->find_item( player_character.inv->invlet_to_position(
1842 key ) ).display_name();
1843 if( hint_text == "none" ) {
1844 // We couldn't find this item in the inventory, let's check worn items
1845 for( const auto &item : player_character.worn ) {
1846 if( item.invlet == key ) {
1847 hint_text = item.display_name();
1848 break;
1849 }
1850 }
1851 }
1852 if( hint_text == "none" ) {
1853 // We couldn't find it in worn items either, must be weapon held
1854 if( player_character.weapon.invlet == key ) {
1855 hint_text = player_character.weapon.display_name();
1856 }
1857 }
1858 } else {
1859 // All other screens - try and show the action name, either from registered actions or manually registered keys
1860 hint_text = touch_input_context.get_action_name( touch_input_context.input_to_action( event ) );
1861 if( hint_text == "ERROR" ) {
1862 hint_text = touch_input_context.get_action_name_for_manual_key( key );
1863 }
1864 }
1865 if( hint_text == "ERROR" || hint_text == "none" || hint_text.empty() ) {
1866 show_hint = false;
1867 }
1868 }
1869 if( shortcut_right )
1870 rect = { WindowWidth - static_cast<int>( ( i + 1 ) * width + border ), static_cast<int>( WindowHeight - height ), static_cast<int>( width - border * 2 ), static_cast<int>( height ) };
1871 else
1872 rect = { static_cast<int>( i * width + border ), static_cast<int>( WindowHeight - height ), static_cast<int>( width - border * 2 ), static_cast<int>( height ) };
1873 if( hovered ) {
1874 SetRenderDrawColor( renderer, 0, 0, 0, 255 );
1875 } else {
1876 SetRenderDrawColor( renderer, 0, 0, 0,
1877 get_option<int>( "ANDROID_SHORTCUT_OPACITY_BG" ) * 0.01f * 255.0f );
1878 }
1879 SetRenderDrawBlendMode( renderer, SDL_BLENDMODE_BLEND );
1880 RenderFillRect( renderer, &rect );
1881 if( hovered ) {
1882 // draw a second button hovering above the first one
1883 if( shortcut_right )
1884 rect = { WindowWidth - static_cast<int>( ( i + 1 ) * width + border ), static_cast<int>( WindowHeight - height * 2.2f ), static_cast<int>( width - border * 2 ), static_cast<int>( height ) };
1885 else
1886 rect = { static_cast<int>( i * width + border ), static_cast<int>( WindowHeight - height * 2.2f ), static_cast<int>( width - border * 2 ), static_cast<int>( height ) };
1887 SetRenderDrawColor( renderer, 0, 0, 196, 255 );
1888 RenderFillRect( renderer, &rect );
1889
1890 if( show_hint ) {
1891 // draw a backdrop for the hint text
1892 rect = { 0, static_cast<int>( ( WindowHeight - height ) * 0.5f ), static_cast<int>( WindowWidth ), static_cast<int>( height ) };
1893 SetRenderDrawColor( renderer, 0, 0, 0,
1894 get_option<int>( "ANDROID_SHORTCUT_OPACITY_BG" ) * 0.01f * 255.0f );
1895 RenderFillRect( renderer, &rect );
1896 }
1897 }
1898 SetRenderDrawBlendMode( renderer, SDL_BLENDMODE_NONE );
1899 SDL_RenderSetScale( renderer.get(), text_scale, text_scale );
1900 int text_x, text_y;
1901 if( shortcut_right ) {
1902 text_x = ( WindowWidth - ( i + 0.5f ) * width - ( font->width * utf8_width(
1903 text ) ) * text_scale * 0.5f ) / text_scale;
1904 } else {
1905 text_x = ( ( i + 0.5f ) * width - ( font->width * utf8_width( text ) ) * text_scale * 0.5f ) /
1906 text_scale;
1907 }
1908 text_y = ( WindowHeight - ( height + font->height * text_scale ) * 0.5f ) / text_scale;
1909 font->OutputChar( renderer, geometry, text, point( text_x + 1, text_y + 1 ), 0,
1910 get_option<int>( "ANDROID_SHORTCUT_OPACITY_SHADOW" ) * 0.01f );
1911 font->OutputChar( renderer, geometry, text, point( text_x, text_y ),
1912 get_option<int>( "ANDROID_SHORTCUT_COLOR" ),
1913 get_option<int>( "ANDROID_SHORTCUT_OPACITY_FG" ) * 0.01f );
1914 if( hovered ) {
1915 // draw a second button hovering above the first one
1916 font->OutputChar( renderer, geometry, text,
1917 point( text_x, text_y - ( height * 1.2f / text_scale ) ),
1918 get_option<int>( "ANDROID_SHORTCUT_COLOR" ) );
1919 if( show_hint ) {
1920 // draw hint text
1921 text_scale = default_text_scale;
1922 hint_text = text + " " + hint_text;
1923 hint_text = remove_color_tags( hint_text );
1924 const float safe_margin = 0.9f;
1925 int hint_length = utf8_width( hint_text );
1926 if( WindowWidth * safe_margin < font->width * text_scale * hint_length ) {
1927 text_scale *= ( WindowWidth * safe_margin ) / ( font->width * text_scale *
1928 hint_length ); // scale to fit comfortably
1929 }
1930 SDL_RenderSetScale( renderer.get(), text_scale, text_scale );
1931 text_x = ( WindowWidth - ( ( font->width * hint_length ) * text_scale ) ) * 0.5f / text_scale;
1932 text_y = ( WindowHeight - font->height * text_scale ) * 0.5f / text_scale;
1933 font->OutputChar( renderer, geometry, hint_text, point( text_x + 1, text_y + 1 ), 0,
1934 get_option<int>( "ANDROID_SHORTCUT_OPACITY_SHADOW" ) * 0.01f );
1935 font->OutputChar( renderer, geometry, hint_text, point( text_x, text_y ),
1936 get_option<int>( "ANDROID_SHORTCUT_COLOR" ),
1937 get_option<int>( "ANDROID_SHORTCUT_OPACITY_FG" ) * 0.01f );
1938 }
1939 }
1940 SDL_RenderSetScale( renderer.get(), 1.0f, 1.0f );
1941 i++;
1942 if( ( i + 1 ) * width > WindowWidth ) {
1943 break;
1944 }
1945 }
1946 }
1947
draw_virtual_joystick()1948 void draw_virtual_joystick()
1949 {
1950
1951 // Bail out if we don't need to draw the joystick
1952 if( !get_option<bool>( "ANDROID_SHOW_VIRTUAL_JOYSTICK" ) ||
1953 finger_down_time <= 0 ||
1954 SDL_GetTicks() - finger_down_time <= static_cast<uint32_t>
1955 ( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ||
1956 is_quick_shortcut_touch ||
1957 is_two_finger_touch ) {
1958 return;
1959 }
1960
1961 SDL_SetTextureAlphaMod( touch_joystick.get(),
1962 get_option<int>( "ANDROID_VIRTUAL_JOYSTICK_OPACITY" ) * 0.01f * 255.0f );
1963
1964 float longest_window_edge = std::max( WindowWidth, WindowHeight );
1965
1966 SDL_Rect dstrect;
1967
1968 // Draw deadzone range
1969 dstrect.w = dstrect.h = ( get_option<float>( "ANDROID_DEADZONE_RANGE" ) ) * longest_window_edge * 2;
1970 dstrect.x = finger_down_x - dstrect.w / 2;
1971 dstrect.y = finger_down_y - dstrect.h / 2;
1972 RenderCopy( renderer, touch_joystick, NULL, &dstrect );
1973
1974 // Draw repeat delay range
1975 dstrect.w = dstrect.h = ( get_option<float>( "ANDROID_DEADZONE_RANGE" ) +
1976 get_option<float>( "ANDROID_REPEAT_DELAY_RANGE" ) ) * longest_window_edge * 2;
1977 dstrect.x = finger_down_x - dstrect.w / 2;
1978 dstrect.y = finger_down_y - dstrect.h / 2;
1979 RenderCopy( renderer, touch_joystick, NULL, &dstrect );
1980
1981 // Draw current touch position (50% size of repeat delay range)
1982 dstrect.w = dstrect.h = dstrect.w / 2;
1983 dstrect.x = finger_down_x + ( finger_curr_x - finger_down_x ) / 2 - dstrect.w / 2;
1984 dstrect.y = finger_down_y + ( finger_curr_y - finger_down_y ) / 2 - dstrect.h / 2;
1985 RenderCopy( renderer, touch_joystick, NULL, &dstrect );
1986
1987 }
1988
update_finger_repeat_delay()1989 void update_finger_repeat_delay()
1990 {
1991 float delta_x = finger_curr_x - finger_down_x;
1992 float delta_y = finger_curr_y - finger_down_y;
1993 float dist = std::sqrt( delta_x * delta_x + delta_y * delta_y );
1994 float longest_window_edge = std::max( WindowWidth, WindowHeight );
1995 float t = clamp<float>( ( dist - ( get_option<float>( "ANDROID_DEADZONE_RANGE" ) *
1996 longest_window_edge ) ) /
1997 std::max( 0.01f, ( get_option<float>( "ANDROID_REPEAT_DELAY_RANGE" ) ) * longest_window_edge ),
1998 0.0f, 1.0f );
1999 float repeat_delay_min = static_cast<float>( get_option<int>( "ANDROID_REPEAT_DELAY_MIN" ) );
2000 float repeat_delay_max = static_cast<float>( get_option<int>( "ANDROID_REPEAT_DELAY_MAX" ) );
2001 finger_repeat_delay = lerp<float>( std::max( repeat_delay_min, repeat_delay_max ),
2002 std::min( repeat_delay_min, repeat_delay_max ),
2003 std::pow( t, get_option<float>( "ANDROID_SENSITIVITY_POWER" ) ) );
2004 }
2005
2006 // TODO: Is there a better way to detect when string entry is allowed?
2007 // ANY_INPUT seems close but is abused by code everywhere.
2008 // Had a look through and think I've got all the cases but can't be 100% sure.
is_string_input(input_context & ctx)2009 bool is_string_input( input_context &ctx )
2010 {
2011 std::string &category = ctx.get_category();
2012 return category == "STRING_INPUT"
2013 || category == "HELP_KEYBINDINGS"
2014 || category == "NEW_CHAR_DESCRIPTION"
2015 || category == "WORLDGEN_CONFIRM_DIALOG";
2016 }
2017
get_key_event_from_string(const std::string & str)2018 int get_key_event_from_string( const std::string &str )
2019 {
2020 if( !str.empty() ) {
2021 return str[0];
2022 }
2023 return -1;
2024 }
2025 // This function is triggered on finger up events, OR by a repeating timer for touch hold events.
handle_finger_input(uint32_t ticks)2026 void handle_finger_input( uint32_t ticks )
2027 {
2028
2029 float delta_x = finger_curr_x - finger_down_x;
2030 float delta_y = finger_curr_y - finger_down_y;
2031 float dist = std::sqrt( delta_x * delta_x + delta_y * delta_y ); // in pixel space
2032 bool handle_diagonals = touch_input_context.is_action_registered( "LEFTUP" );
2033 bool is_default_mode = touch_input_context.get_category() == "DEFAULTMODE";
2034 if( dist > ( get_option<float>( "ANDROID_DEADZONE_RANGE" )*std::max( WindowWidth,
2035 WindowHeight ) ) ) {
2036 if( !handle_diagonals ) {
2037 if( delta_x >= 0 && delta_y >= 0 ) {
2038 last_input = input_event( delta_x > delta_y ? KEY_RIGHT : KEY_DOWN, input_event_t::keyboard_char );
2039 } else if( delta_x < 0 && delta_y >= 0 ) {
2040 last_input = input_event( -delta_x > delta_y ? KEY_LEFT : KEY_DOWN, input_event_t::keyboard_char );
2041 } else if( delta_x >= 0 && delta_y < 0 ) {
2042 last_input = input_event( delta_x > -delta_y ? KEY_RIGHT : KEY_UP, input_event_t::keyboard_char );
2043 } else if( delta_x < 0 && delta_y < 0 ) {
2044 last_input = input_event( -delta_x > -delta_y ? KEY_LEFT : KEY_UP, input_event_t::keyboard_char );
2045 }
2046 } else {
2047 if( delta_x > 0 ) {
2048 if( std::abs( delta_y ) < delta_x * 0.5f ) {
2049 // swipe right
2050 last_input = input_event( KEY_RIGHT, input_event_t::keyboard_char );
2051 } else if( std::abs( delta_y ) < delta_x * 2.0f ) {
2052 if( delta_y < 0 ) {
2053 // swipe up-right
2054 last_input = input_event( JOY_RIGHTUP, input_event_t::gamepad );
2055 } else {
2056 // swipe down-right
2057 last_input = input_event( JOY_RIGHTDOWN, input_event_t::gamepad );
2058 }
2059 } else {
2060 if( delta_y < 0 ) {
2061 // swipe up
2062 last_input = input_event( KEY_UP, input_event_t::keyboard_char );
2063 } else {
2064 // swipe down
2065 last_input = input_event( KEY_DOWN, input_event_t::keyboard_char );
2066 }
2067 }
2068 } else {
2069 if( std::abs( delta_y ) < -delta_x * 0.5f ) {
2070 // swipe left
2071 last_input = input_event( KEY_LEFT, input_event_t::keyboard_char );
2072 } else if( std::abs( delta_y ) < -delta_x * 2.0f ) {
2073 if( delta_y < 0 ) {
2074 // swipe up-left
2075 last_input = input_event( JOY_LEFTUP, input_event_t::gamepad );
2076
2077 } else {
2078 // swipe down-left
2079 last_input = input_event( JOY_LEFTDOWN, input_event_t::gamepad );
2080 }
2081 } else {
2082 if( delta_y < 0 ) {
2083 // swipe up
2084 last_input = input_event( KEY_UP, input_event_t::keyboard_char );
2085 } else {
2086 // swipe down
2087 last_input = input_event( KEY_DOWN, input_event_t::keyboard_char );
2088 }
2089 }
2090 }
2091 }
2092 } else {
2093 if( ticks - finger_down_time >= static_cast<uint32_t>
2094 ( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2095 // Single tap (repeat) - held, so always treat this as a tap
2096 // We only allow repeats for waiting, not confirming in menus as that's a bit silly
2097 if( is_default_mode ) {
2098 last_input = input_event( get_key_event_from_string( get_option<std::string>( "ANDROID_TAP_KEY" ) ),
2099 input_event_t::keyboard_char );
2100 }
2101 } else {
2102 if( last_tap_time > 0 &&
2103 ticks - last_tap_time < static_cast<uint32_t>( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2104 // Double tap
2105 last_input = input_event( is_default_mode ? KEY_ESCAPE : KEY_ESCAPE, input_event_t::keyboard_char );
2106 last_tap_time = 0;
2107 } else {
2108 // First tap detected, waiting to decide whether it's a single or a double tap input
2109 last_tap_time = ticks;
2110 }
2111 }
2112 }
2113 }
2114
android_is_hardware_keyboard_available()2115 bool android_is_hardware_keyboard_available()
2116 {
2117 JNIEnv *env = ( JNIEnv * )SDL_AndroidGetJNIEnv();
2118 jobject activity = ( jobject )SDL_AndroidGetActivity();
2119 jclass clazz( env->GetObjectClass( activity ) );
2120 jmethodID method_id = env->GetMethodID( clazz, "isHardwareKeyboardAvailable", "()Z" );
2121 jboolean ans = env->CallBooleanMethod( activity, method_id );
2122 env->DeleteLocalRef( activity );
2123 env->DeleteLocalRef( clazz );
2124 return ans;
2125 }
2126
android_vibrate()2127 void android_vibrate()
2128 {
2129 int vibration_ms = get_option<int>( "ANDROID_VIBRATION" );
2130 if( vibration_ms > 0 && !android_is_hardware_keyboard_available() ) {
2131 JNIEnv *env = ( JNIEnv * )SDL_AndroidGetJNIEnv();
2132 jobject activity = ( jobject )SDL_AndroidGetActivity();
2133 jclass clazz( env->GetObjectClass( activity ) );
2134 jmethodID method_id = env->GetMethodID( clazz, "vibrate", "(I)V" );
2135 env->CallVoidMethod( activity, method_id, vibration_ms );
2136 env->DeleteLocalRef( activity );
2137 env->DeleteLocalRef( clazz );
2138 }
2139 }
2140 #endif
2141
2142 //Check for any window messages (keypress, paint, mousemove, etc)
CheckMessages()2143 static void CheckMessages()
2144 {
2145 SDL_Event ev;
2146 bool quit = false;
2147 bool text_refresh = false;
2148 bool is_repeat = false;
2149 if( HandleDPad() ) {
2150 return;
2151 }
2152
2153 #if defined(__ANDROID__)
2154 if( visible_display_frame_dirty ) {
2155 needupdate = true;
2156 visible_display_frame_dirty = false;
2157 }
2158
2159 uint32_t ticks = SDL_GetTicks();
2160
2161 // Force text input mode if hardware keyboard is available.
2162 if( android_is_hardware_keyboard_available() && !SDL_IsTextInputActive() ) {
2163 SDL_StartTextInput();
2164 }
2165
2166 // Make sure the SDL surface view is visible, otherwise the "Z" loading screen is visible.
2167 if( needs_sdl_surface_visibility_refresh ) {
2168 needs_sdl_surface_visibility_refresh = false;
2169
2170 // Call Java show_sdl_surface()
2171 JNIEnv *env = ( JNIEnv * )SDL_AndroidGetJNIEnv();
2172 jobject activity = ( jobject )SDL_AndroidGetActivity();
2173 jclass clazz( env->GetObjectClass( activity ) );
2174 jmethodID method_id = env->GetMethodID( clazz, "show_sdl_surface", "()V" );
2175 env->CallVoidMethod( activity, method_id );
2176 env->DeleteLocalRef( activity );
2177 env->DeleteLocalRef( clazz );
2178 }
2179
2180 // Copy the current input context
2181 if( !input_context::input_context_stack.empty() ) {
2182 input_context *new_input_context = *--input_context::input_context_stack.end();
2183 if( new_input_context && *new_input_context != touch_input_context ) {
2184
2185 // If we were in an allow_text_entry input context, and text input is still active, and we're auto-managing keyboard, hide it.
2186 if( touch_input_context.allow_text_entry &&
2187 !new_input_context->allow_text_entry &&
2188 !is_string_input( *new_input_context ) &&
2189 SDL_IsTextInputActive() &&
2190 get_option<bool>( "ANDROID_AUTO_KEYBOARD" ) ) {
2191 SDL_StopTextInput();
2192 }
2193
2194 touch_input_context = *new_input_context;
2195 needupdate = true;
2196 }
2197 }
2198
2199 bool is_default_mode = touch_input_context.get_category() == "DEFAULTMODE";
2200 quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name(
2201 touch_input_context.get_category() )];
2202
2203 // Don't do this logic if we already need an update, otherwise we're likely to overload the game with too much input on hold repeat events
2204 if( !needupdate ) {
2205
2206 // Check action weightings and auto-add any immediate-surrounding actions as quick shortcuts
2207 // This code is based heavily off action.cpp handle_action_menu() which puts common shortcuts at the top
2208 if( is_default_mode && get_option<bool>( "ANDROID_SHORTCUT_AUTOADD" ) ) {
2209 static int last_moves_since_last_save = -1;
2210 if( last_moves_since_last_save != g->get_moves_since_last_save() ) {
2211 last_moves_since_last_save = g->get_moves_since_last_save();
2212
2213 // Actions to add
2214 std::set<action_id> actions;
2215
2216 // Actions to remove - we only want to remove things that we're 100% sure won't be useful to players otherwise
2217 std::set<action_id> actions_remove;
2218
2219 Character &player_character = get_player_character();
2220 // Check if we're in a potential combat situation, if so, sort a few actions to the top.
2221 if( !player_character.get_hostile_creatures( 60 ).empty() ) {
2222 // Only prioritize movement options if we're not driving.
2223 if( !player_character.controlling_vehicle ) {
2224 actions.insert( ACTION_CYCLE_MOVE );
2225 }
2226 // Only prioritize fire weapon options if we're wielding a ranged weapon.
2227 if( player_character.weapon.is_gun() || player_character.weapon.has_flag( flag_REACH_ATTACK ) ) {
2228 actions.insert( ACTION_FIRE );
2229 }
2230 }
2231
2232 // If we're already running, make it simple to toggle running to off.
2233 if( player_character.is_running() ) {
2234 actions.insert( ACTION_TOGGLE_RUN );
2235 }
2236 // If we're already crouching, make it simple to toggle crouching to off.
2237 if( player_character.is_crouching() ) {
2238 actions.insert( ACTION_TOGGLE_CROUCH );
2239 }
2240
2241 // We're not already running or in combat, so remove cycle walk/run
2242 if( std::find( actions.begin(), actions.end(), ACTION_CYCLE_MOVE ) == actions.end() ) {
2243 actions_remove.insert( ACTION_CYCLE_MOVE );
2244 }
2245
2246 map &here = get_map();
2247 // Check if we can perform one of our actions on nearby terrain. If so,
2248 // display that action at the top of the list.
2249 for( int dx = -1; dx <= 1; dx++ ) {
2250 for( int dy = -1; dy <= 1; dy++ ) {
2251 int x = player_character.posx() + dx;
2252 int y = player_character.posy() + dy;
2253 int z = player_character.posz();
2254 const tripoint pos( x, y, z );
2255
2256 // Check if we're near a vehicle, if so, vehicle controls should be top.
2257 {
2258 const optional_vpart_position vp = here.veh_at( pos );
2259 vehicle *const veh = veh_pointer_or_null( vp );
2260 if( veh ) {
2261 const int veh_part = vp ? vp->part_index() : -1;
2262 if( veh->part_with_feature( veh_part, "CONTROLS", true ) >= 0 ) {
2263 actions.insert( ACTION_CONTROL_VEHICLE );
2264 }
2265 const int openablepart = veh->part_with_feature( veh_part, "OPENABLE", true );
2266 if( openablepart >= 0 && veh->is_open( openablepart ) && ( dx != 0 ||
2267 dy != 0 ) ) { // an open door adjacent to us
2268 actions.insert( ACTION_CLOSE );
2269 }
2270 const int curtainpart = veh->part_with_feature( veh_part, "CURTAIN", true );
2271 if( curtainpart >= 0 && veh->is_open( curtainpart ) && ( dx != 0 || dy != 0 ) ) {
2272 actions.insert( ACTION_CLOSE );
2273 }
2274 const int cargopart = veh->part_with_feature( veh_part, "CARGO", true );
2275 if( cargopart >= 0 && ( !veh->get_items( cargopart ).empty() ) ) {
2276 actions.insert( ACTION_PICKUP );
2277 }
2278 }
2279 }
2280
2281 if( dx != 0 || dy != 0 ) {
2282 // Check for actions that work on nearby tiles
2283 //if( can_interact_at( ACTION_OPEN, pos ) ) {
2284 // don't bother with open since user can just walk into target
2285 //}
2286 if( can_interact_at( ACTION_CLOSE, pos ) ) {
2287 actions.insert( ACTION_CLOSE );
2288 }
2289 if( can_interact_at( ACTION_EXAMINE, pos ) ) {
2290 actions.insert( ACTION_EXAMINE );
2291 }
2292 } else {
2293 // Check for actions that work on own tile only
2294 if( can_interact_at( ACTION_BUTCHER, pos ) ) {
2295 actions.insert( ACTION_BUTCHER );
2296 } else {
2297 actions_remove.insert( ACTION_BUTCHER );
2298 }
2299
2300 if( can_interact_at( ACTION_MOVE_UP, pos ) ) {
2301 actions.insert( ACTION_MOVE_UP );
2302 } else {
2303 actions_remove.insert( ACTION_MOVE_UP );
2304 }
2305
2306 if( can_interact_at( ACTION_MOVE_DOWN, pos ) ) {
2307 actions.insert( ACTION_MOVE_DOWN );
2308 } else {
2309 actions_remove.insert( ACTION_MOVE_DOWN );
2310 }
2311 }
2312
2313 // Check for actions that work on nearby tiles and own tile
2314 if( can_interact_at( ACTION_PICKUP, pos ) ) {
2315 actions.insert( ACTION_PICKUP );
2316 }
2317 }
2318 }
2319
2320 // We're not near a vehicle, so remove control vehicle
2321 if( std::find( actions.begin(), actions.end(), ACTION_CONTROL_VEHICLE ) == actions.end() ) {
2322 actions_remove.insert( ACTION_CONTROL_VEHICLE );
2323 }
2324
2325 // We're not able to close anything nearby, so remove it
2326 if( std::find( actions.begin(), actions.end(), ACTION_CLOSE ) == actions.end() ) {
2327 actions_remove.insert( ACTION_CLOSE );
2328 }
2329
2330 // We're not able to examine anything nearby, so remove it
2331 if( std::find( actions.begin(), actions.end(), ACTION_EXAMINE ) == actions.end() ) {
2332 actions_remove.insert( ACTION_EXAMINE );
2333 }
2334
2335 // We're not able to pickup anything nearby, so remove it
2336 if( std::find( actions.begin(), actions.end(), ACTION_PICKUP ) == actions.end() ) {
2337 actions_remove.insert( ACTION_PICKUP );
2338 }
2339
2340 // Check if we can't move because of safe mode - if so, add ability to ignore
2341 if( g && !g->check_safe_mode_allowed( false ) ) {
2342 actions.insert( ACTION_IGNORE_ENEMY );
2343 actions.insert( ACTION_TOGGLE_SAFEMODE );
2344 } else {
2345 actions_remove.insert( ACTION_IGNORE_ENEMY );
2346 actions_remove.insert( ACTION_TOGGLE_SAFEMODE );
2347 }
2348
2349 // Check if we're significantly hungry or thirsty - if so, add eat
2350 if( player_character.get_hunger() > 100 || player_character.get_thirst() > 40 ) {
2351 actions.insert( ACTION_EAT );
2352 }
2353
2354 // Check if we're dead tired - if so, add sleep
2355 if( player_character.get_fatigue() > fatigue_levels::DEAD_TIRED ) {
2356 actions.insert( ACTION_SLEEP );
2357 }
2358
2359 for( const auto &action : actions ) {
2360 if( add_best_key_for_action_to_quick_shortcuts( action, touch_input_context.get_category(),
2361 !get_option<bool>( "ANDROID_SHORTCUT_AUTOADD_FRONT" ) ) ) {
2362 needupdate = true;
2363 }
2364 }
2365
2366 size_t old_size = qsl.size();
2367 for( const auto &action_remove : actions_remove ) {
2368 remove_action_from_quick_shortcuts( action_remove, touch_input_context.get_category() );
2369 }
2370 if( qsl.size() != old_size ) {
2371 needupdate = true;
2372 }
2373 }
2374 }
2375
2376 if( remove_expired_actions_from_quick_shortcuts( touch_input_context.get_category() ) ) {
2377 needupdate = true;
2378 }
2379
2380 // Toggle quick shortcuts on/off
2381 if( ac_back_down_time > 0 &&
2382 ticks - ac_back_down_time > static_cast<uint32_t>
2383 ( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2384 if( !quick_shortcuts_toggle_handled ) {
2385 quick_shortcuts_enabled = !quick_shortcuts_enabled;
2386 quick_shortcuts_toggle_handled = true;
2387 refresh_display();
2388
2389 // Display an Android toast message
2390 {
2391 JNIEnv *env = ( JNIEnv * )SDL_AndroidGetJNIEnv();
2392 jobject activity = ( jobject )SDL_AndroidGetActivity();
2393 jclass clazz( env->GetObjectClass( activity ) );
2394 jstring toast_message = env->NewStringUTF( quick_shortcuts_enabled ? "Shortcuts visible" :
2395 "Shortcuts hidden" );
2396 jmethodID method_id = env->GetMethodID( clazz, "toast", "(Ljava/lang/String;)V" );
2397 env->CallVoidMethod( activity, method_id, toast_message );
2398 env->DeleteLocalRef( activity );
2399 env->DeleteLocalRef( clazz );
2400 }
2401 }
2402 }
2403
2404 // Handle repeating inputs from touch + holds
2405 if( !is_quick_shortcut_touch && !is_two_finger_touch && finger_down_time > 0 &&
2406 ticks - finger_down_time > static_cast<uint32_t>
2407 ( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2408 if( ticks - finger_repeat_time > finger_repeat_delay ) {
2409 handle_finger_input( ticks );
2410 finger_repeat_time = ticks;
2411 // Prevent repeating inputs on the next call to this function if there is a fingerup event
2412 while( SDL_PollEvent( &ev ) ) {
2413 if( ev.type == SDL_FINGERUP ) {
2414 second_finger_down_x = second_finger_curr_x = finger_down_x = finger_curr_x = -1.0f;
2415 second_finger_down_y = second_finger_curr_y = finger_down_y = finger_curr_y = -1.0f;
2416 is_two_finger_touch = false;
2417 finger_down_time = 0;
2418 finger_repeat_time = 0;
2419 // let the next call decide if needupdate should be true
2420 break;
2421 }
2422 }
2423 return;
2424 }
2425 }
2426
2427 // If we received a first tap and not another one within a certain period, this was a single tap, so trigger the input event
2428 if( !is_quick_shortcut_touch && !is_two_finger_touch && last_tap_time > 0 &&
2429 ticks - last_tap_time >= static_cast<uint32_t>
2430 ( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2431 // Single tap
2432 last_tap_time = ticks;
2433 last_input = input_event( is_default_mode ? get_key_event_from_string(
2434 get_option<std::string>( "ANDROID_TAP_KEY" ) ) : '\n', input_event_t::keyboard_char );
2435 last_tap_time = 0;
2436 return;
2437 }
2438
2439 // ensure hint text pops up even if player doesn't move finger to trigger a FINGERMOTION event
2440 if( is_quick_shortcut_touch && finger_down_time > 0 &&
2441 ticks - finger_down_time > static_cast<uint32_t>
2442 ( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2443 needupdate = true;
2444 }
2445 }
2446 #endif
2447
2448 last_input = input_event();
2449
2450 bool need_redraw = false;
2451
2452 while( SDL_PollEvent( &ev ) ) {
2453 switch( ev.type ) {
2454 case SDL_WINDOWEVENT:
2455 switch( ev.window.event ) {
2456 #if defined(__ANDROID__)
2457 // SDL will send a focus lost event whenever the app loses focus (eg. lock screen, switch app focus etc.)
2458 // If we detect it and the game seems in a saveable state, try and do a quicksave. This is a bit dodgy
2459 // as the player could be ANYWHERE doing ANYTHING (a sub-menu, interacting with an NPC/computer etc.)
2460 // but it seems to work so far, and the alternative is the player losing their progress as the app is likely
2461 // to be destroyed pretty quickly when it goes out of focus due to memory usage.
2462 case SDL_WINDOWEVENT_FOCUS_LOST:
2463 if( world_generator &&
2464 world_generator->active_world &&
2465 g && g->uquit == QUIT_NO &&
2466 get_option<bool>( "ANDROID_QUICKSAVE" ) &&
2467 !std::uncaught_exception() ) {
2468 g->quicksave();
2469 }
2470 break;
2471 // SDL sends a window size changed event whenever the screen rotates orientation
2472 case SDL_WINDOWEVENT_SIZE_CHANGED:
2473 WindowWidth = ev.window.data1;
2474 WindowHeight = ev.window.data2;
2475 SDL_Delay( 500 );
2476 SDL_GetWindowSurface( window.get() );
2477 refresh_display();
2478 needupdate = true;
2479 break;
2480 #endif
2481 case SDL_WINDOWEVENT_SHOWN:
2482 case SDL_WINDOWEVENT_MINIMIZED:
2483 case SDL_WINDOWEVENT_FOCUS_GAINED:
2484 break;
2485 case SDL_WINDOWEVENT_EXPOSED:
2486 need_redraw = true;
2487 needupdate = true;
2488 break;
2489 case SDL_WINDOWEVENT_RESTORED:
2490 #if defined(__ANDROID__)
2491 needs_sdl_surface_visibility_refresh = true;
2492 if( android_is_hardware_keyboard_available() ) {
2493 SDL_StopTextInput();
2494 SDL_StartTextInput();
2495 }
2496 #endif
2497 break;
2498 case SDL_WINDOWEVENT_RESIZED: {
2499 restore_on_out_of_scope<input_event> prev_last_input( last_input );
2500 needupdate = handle_resize( ev.window.data1, ev.window.data2 );
2501 break;
2502 }
2503 default:
2504 break;
2505 }
2506 break;
2507 case SDL_RENDER_TARGETS_RESET:
2508 need_redraw = true;
2509 needupdate = true;
2510 break;
2511 case SDL_KEYDOWN: {
2512 #if defined(__ANDROID__)
2513 // Toggle virtual keyboard with Android back button. For some reason I get double inputs, so ignore everything once it's already down.
2514 if( ev.key.keysym.sym == SDLK_AC_BACK && ac_back_down_time == 0 ) {
2515 ac_back_down_time = ticks;
2516 quick_shortcuts_toggle_handled = false;
2517 }
2518 #endif
2519 is_repeat = ev.key.repeat;
2520 //hide mouse cursor on keyboard input
2521 if( get_option<std::string>( "HIDE_CURSOR" ) != "show" && SDL_ShowCursor( -1 ) ) {
2522 SDL_ShowCursor( SDL_DISABLE );
2523 }
2524 keyboard_mode mode = keyboard_mode::keychar;
2525 #if !defined(__ANDROID__) && !defined(TARGET_OS_IPHONE)
2526 if( !SDL_IsTextInputActive() ) {
2527 mode = keyboard_mode::keycode;
2528 }
2529 #endif
2530 if( mode == keyboard_mode::keychar ) {
2531 const int lc = sdl_keysym_to_curses( ev.key.keysym );
2532 if( lc <= 0 ) {
2533 // a key we don't know in curses and won't handle.
2534 break;
2535 } else if( add_alt_code( lc ) ) {
2536 // key was handled
2537 } else {
2538 last_input = input_event( lc, input_event_t::keyboard_char );
2539 #if defined(__ANDROID__)
2540 if( !android_is_hardware_keyboard_available() ) {
2541 if( !is_string_input( touch_input_context ) && !touch_input_context.allow_text_entry ) {
2542 if( get_option<bool>( "ANDROID_AUTO_KEYBOARD" ) ) {
2543 SDL_StopTextInput();
2544 }
2545
2546 // add a quick shortcut
2547 if( !last_input.text.empty() ||
2548 !inp_mngr.get_keyname( lc, input_event_t::keyboard_char ).empty() ) {
2549 qsl.remove( last_input );
2550 add_quick_shortcut( qsl, last_input, false, true );
2551 refresh_display();
2552 }
2553 } else if( lc == '\n' || lc == KEY_ESCAPE ) {
2554 if( get_option<bool>( "ANDROID_AUTO_KEYBOARD" ) ) {
2555 SDL_StopTextInput();
2556 }
2557 }
2558 }
2559 #endif
2560 }
2561 } else {
2562 last_input = sdl_keysym_to_keycode_evt( ev.key.keysym );
2563 }
2564 }
2565 break;
2566 case SDL_KEYUP: {
2567 #if defined(__ANDROID__)
2568 // Toggle virtual keyboard with Android back button
2569 if( ev.key.keysym.sym == SDLK_AC_BACK ) {
2570 if( ticks - ac_back_down_time <= static_cast<uint32_t>
2571 ( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2572 if( SDL_IsTextInputActive() ) {
2573 SDL_StopTextInput();
2574 } else {
2575 SDL_StartTextInput();
2576 }
2577 }
2578 ac_back_down_time = 0;
2579 }
2580 #endif
2581 keyboard_mode mode = keyboard_mode::keychar;
2582 #if !defined(__ANDROID__) && !defined(TARGET_OS_IPHONE)
2583 if( !SDL_IsTextInputActive() ) {
2584 mode = keyboard_mode::keycode;
2585 }
2586 #endif
2587 is_repeat = ev.key.repeat;
2588 if( mode == keyboard_mode::keychar ) {
2589 if( ev.key.keysym.sym == SDLK_LALT || ev.key.keysym.sym == SDLK_RALT ) {
2590 int code = end_alt_code();
2591 if( code ) {
2592 last_input = input_event( code, input_event_t::keyboard_char );
2593 last_input.text = utf32_to_utf8( code );
2594 }
2595 }
2596 } else if( is_repeat ) {
2597 last_input = sdl_keysym_to_keycode_evt( ev.key.keysym );
2598 }
2599 }
2600 break;
2601 case SDL_TEXTINPUT:
2602 if( !add_alt_code( *ev.text.text ) ) {
2603 if( strlen( ev.text.text ) > 0 ) {
2604 const unsigned lc = UTF8_getch( ev.text.text );
2605 last_input = input_event( lc, input_event_t::keyboard_char );
2606 #if defined(__ANDROID__)
2607 if( !android_is_hardware_keyboard_available() ) {
2608 if( !is_string_input( touch_input_context ) && !touch_input_context.allow_text_entry ) {
2609 if( get_option<bool>( "ANDROID_AUTO_KEYBOARD" ) ) {
2610 SDL_StopTextInput();
2611 }
2612
2613 quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name(
2614 touch_input_context.get_category() )];
2615 qsl.remove( last_input );
2616 add_quick_shortcut( qsl, last_input, false, true );
2617 refresh_display();
2618 } else if( lc == '\n' || lc == KEY_ESCAPE ) {
2619 if( get_option<bool>( "ANDROID_AUTO_KEYBOARD" ) ) {
2620 SDL_StopTextInput();
2621 }
2622 }
2623 }
2624 #endif
2625 } else {
2626 // no key pressed in this event
2627 last_input = input_event();
2628 last_input.type = input_event_t::keyboard_char;
2629 }
2630 last_input.text = ev.text.text;
2631 text_refresh = true;
2632 }
2633 break;
2634 case SDL_TEXTEDITING: {
2635 if( strlen( ev.edit.text ) > 0 ) {
2636 const unsigned lc = UTF8_getch( ev.edit.text );
2637 last_input = input_event( lc, input_event_t::keyboard_char );
2638 } else {
2639 // no key pressed in this event
2640 last_input = input_event();
2641 last_input.type = input_event_t::keyboard_char;
2642 }
2643 last_input.edit = ev.edit.text;
2644 last_input.edit_refresh = true;
2645 text_refresh = true;
2646 }
2647 break;
2648 case SDL_JOYBUTTONDOWN:
2649 last_input = input_event( ev.jbutton.button, input_event_t::keyboard_char );
2650 break;
2651 case SDL_JOYAXISMOTION:
2652 // on gamepads, the axes are the analog sticks
2653 // TODO: somehow get the "digipad" values from the axes
2654 break;
2655 case SDL_MOUSEMOTION:
2656 if( get_option<std::string>( "HIDE_CURSOR" ) == "show" ||
2657 get_option<std::string>( "HIDE_CURSOR" ) == "hidekb" ) {
2658 if( !SDL_ShowCursor( -1 ) ) {
2659 SDL_ShowCursor( SDL_ENABLE );
2660 }
2661
2662 // Only monitor motion when cursor is visible
2663 last_input = input_event( MOUSE_MOVE, input_event_t::mouse );
2664 }
2665 break;
2666
2667 case SDL_MOUSEBUTTONUP:
2668 switch( ev.button.button ) {
2669 case SDL_BUTTON_LEFT:
2670 last_input = input_event( MOUSE_BUTTON_LEFT, input_event_t::mouse );
2671 break;
2672 case SDL_BUTTON_RIGHT:
2673 last_input = input_event( MOUSE_BUTTON_RIGHT, input_event_t::mouse );
2674 break;
2675 }
2676 break;
2677
2678 case SDL_MOUSEWHEEL:
2679 if( ev.wheel.y > 0 ) {
2680 last_input = input_event( SCROLLWHEEL_UP, input_event_t::mouse );
2681 } else if( ev.wheel.y < 0 ) {
2682 last_input = input_event( SCROLLWHEEL_DOWN, input_event_t::mouse );
2683 }
2684 break;
2685
2686 #if defined(__ANDROID__)
2687 case SDL_FINGERMOTION:
2688 if( ev.tfinger.fingerId == 0 ) {
2689 if( !is_quick_shortcut_touch ) {
2690 update_finger_repeat_delay();
2691 }
2692 needupdate = true; // ensure virtual joystick and quick shortcuts redraw as we interact
2693 finger_curr_x = ev.tfinger.x * WindowWidth;
2694 finger_curr_y = ev.tfinger.y * WindowHeight;
2695
2696 if( get_option<bool>( "ANDROID_VIRTUAL_JOYSTICK_FOLLOW" ) && !is_two_finger_touch ) {
2697 // If we've moved too far from joystick center, offset joystick center automatically
2698 float delta_x = finger_curr_x - finger_down_x;
2699 float delta_y = finger_curr_y - finger_down_y;
2700 float dist = std::sqrt( delta_x * delta_x + delta_y * delta_y );
2701 float max_dist = ( get_option<float>( "ANDROID_DEADZONE_RANGE" ) +
2702 get_option<float>( "ANDROID_REPEAT_DELAY_RANGE" ) ) * std::max( WindowWidth, WindowHeight );
2703 if( dist > max_dist ) {
2704 float delta_ratio = ( dist / max_dist ) - 1.0f;
2705 finger_down_x += delta_x * delta_ratio;
2706 finger_down_y += delta_y * delta_ratio;
2707 }
2708 }
2709
2710 } else if( ev.tfinger.fingerId == 1 ) {
2711 second_finger_curr_x = ev.tfinger.x * WindowWidth;
2712 second_finger_curr_y = ev.tfinger.y * WindowHeight;
2713 }
2714 break;
2715 case SDL_FINGERDOWN:
2716 if( ev.tfinger.fingerId == 0 ) {
2717 finger_down_x = finger_curr_x = ev.tfinger.x * WindowWidth;
2718 finger_down_y = finger_curr_y = ev.tfinger.y * WindowHeight;
2719 finger_down_time = ticks;
2720 finger_repeat_time = 0;
2721 is_quick_shortcut_touch = get_quick_shortcut_under_finger() != NULL;
2722 if( !is_quick_shortcut_touch ) {
2723 update_finger_repeat_delay();
2724 }
2725 needupdate = true; // ensure virtual joystick and quick shortcuts redraw as we interact
2726 } else if( ev.tfinger.fingerId == 1 ) {
2727 if( !is_quick_shortcut_touch ) {
2728 second_finger_down_x = second_finger_curr_x = ev.tfinger.x * WindowWidth;
2729 second_finger_down_y = second_finger_curr_y = ev.tfinger.y * WindowHeight;
2730 is_two_finger_touch = true;
2731 }
2732 }
2733 break;
2734 case SDL_FINGERUP:
2735 if( ev.tfinger.fingerId == 0 ) {
2736 finger_curr_x = ev.tfinger.x * WindowWidth;
2737 finger_curr_y = ev.tfinger.y * WindowHeight;
2738 if( is_quick_shortcut_touch ) {
2739 input_event *quick_shortcut = get_quick_shortcut_under_finger();
2740 if( quick_shortcut ) {
2741 last_input = *quick_shortcut;
2742 if( get_option<bool>( "ANDROID_SHORTCUT_MOVE_FRONT" ) ) {
2743 quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name(
2744 touch_input_context.get_category() )];
2745 reorder_quick_shortcut( qsl, quick_shortcut->get_first_input(), false );
2746 }
2747 quick_shortcut->shortcut_last_used_action_counter = g->get_user_action_counter();
2748 } else {
2749 // Get the quick shortcut that was originally touched
2750 quick_shortcut = get_quick_shortcut_under_finger( true );
2751 if( quick_shortcut &&
2752 ticks - finger_down_time <= static_cast<uint32_t>( get_option<int>( "ANDROID_INITIAL_DELAY" ) )
2753 &&
2754 finger_curr_y < finger_down_y &&
2755 finger_down_y - finger_curr_y > std::abs( finger_down_x - finger_curr_x ) ) {
2756 // a flick up was detected, remove the quick shortcut!
2757 quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name(
2758 touch_input_context.get_category() )];
2759 qsl.remove( *quick_shortcut );
2760 }
2761 }
2762 } else {
2763 if( is_two_finger_touch ) {
2764 // handle zoom in/out
2765 if( is_default_mode ) {
2766 float x1 = ( finger_curr_x - finger_down_x );
2767 float y1 = ( finger_curr_y - finger_down_y );
2768 float d1 = std::sqrt( x1 * x1 + y1 * y1 );
2769
2770 float x2 = ( second_finger_curr_x - second_finger_down_x );
2771 float y2 = ( second_finger_curr_y - second_finger_down_y );
2772 float d2 = std::sqrt( x2 * x2 + y2 * y2 );
2773
2774 float longest_window_edge = std::max( WindowWidth, WindowHeight );
2775
2776 if( std::max( d1, d2 ) < get_option<float>( "ANDROID_DEADZONE_RANGE" ) * longest_window_edge ) {
2777 last_input = input_event( get_key_event_from_string(
2778 get_option<std::string>( "ANDROID_2_TAP_KEY" ) ), input_event_t::keyboard_char );
2779 } else {
2780 float dot = ( x1 * x2 + y1 * y2 ) / ( d1 * d2 ); // dot product of two finger vectors, -1 to +1
2781 if( dot > 0.0f ) { // both fingers mostly heading in same direction, check for double-finger swipe gesture
2782 float dratio = d1 / d2;
2783 const float dist_ratio = 0.3f;
2784 if( dratio > dist_ratio &&
2785 dratio < ( 1.0f /
2786 dist_ratio ) ) { // both fingers moved roughly the same distance, so it's a double-finger swipe!
2787 float xavg = 0.5f * ( x1 + x2 );
2788 float yavg = 0.5f * ( y1 + y2 );
2789 if( xavg > 0 && xavg > std::abs( yavg ) ) {
2790 last_input = input_event( get_key_event_from_string(
2791 get_option<std::string>( "ANDROID_2_SWIPE_LEFT_KEY" ) ), input_event_t::keyboard_char );
2792 } else if( xavg < 0 && -xavg > std::abs( yavg ) ) {
2793 last_input = input_event( get_key_event_from_string(
2794 get_option<std::string>( "ANDROID_2_SWIPE_RIGHT_KEY" ) ), input_event_t::keyboard_char );
2795 } else if( yavg > 0 && yavg > std::abs( xavg ) ) {
2796 last_input = input_event( get_key_event_from_string(
2797 get_option<std::string>( "ANDROID_2_SWIPE_DOWN_KEY" ) ), input_event_t::keyboard_char );
2798 } else {
2799 last_input = input_event( get_key_event_from_string(
2800 get_option<std::string>( "ANDROID_2_SWIPE_UP_KEY" ) ), input_event_t::keyboard_char );
2801 }
2802 }
2803 } else {
2804 // both fingers heading in opposite direction, check for zoom gesture
2805 float down_x = finger_down_x - second_finger_down_x;
2806 float down_y = finger_down_y - second_finger_down_y;
2807 float down_dist = std::sqrt( down_x * down_x + down_y * down_y );
2808
2809 float curr_x = finger_curr_x - second_finger_curr_x;
2810 float curr_y = finger_curr_y - second_finger_curr_y;
2811 float curr_dist = std::sqrt( curr_x * curr_x + curr_y * curr_y );
2812
2813 const float zoom_ratio = 0.9f;
2814 if( curr_dist < down_dist * zoom_ratio ) {
2815 last_input = input_event( get_key_event_from_string(
2816 get_option<std::string>( "ANDROID_PINCH_IN_KEY" ) ), input_event_t::keyboard_char );
2817 } else if( curr_dist > down_dist / zoom_ratio ) {
2818 last_input = input_event( get_key_event_from_string(
2819 get_option<std::string>( "ANDROID_PINCH_OUT_KEY" ) ), input_event_t::keyboard_char );
2820 }
2821 }
2822 }
2823 }
2824 } else if( ticks - finger_down_time <= static_cast<uint32_t>(
2825 get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2826 handle_finger_input( ticks );
2827 }
2828 }
2829 second_finger_down_x = second_finger_curr_x = finger_down_x = finger_curr_x = -1.0f;
2830 second_finger_down_y = second_finger_curr_y = finger_down_y = finger_curr_y = -1.0f;
2831 is_two_finger_touch = false;
2832 finger_down_time = 0;
2833 finger_repeat_time = 0;
2834 needupdate = true; // ensure virtual joystick and quick shortcuts are updated properly
2835 refresh_display(); // as above, but actually redraw it now as well
2836 } else if( ev.tfinger.fingerId == 1 ) {
2837 if( is_two_finger_touch ) {
2838 // on second finger release, just remember the x/y position so we can calculate delta once first finger is done
2839 // is_two_finger_touch will be reset when first finger lifts (see above)
2840 second_finger_curr_x = ev.tfinger.x * WindowWidth;
2841 second_finger_curr_y = ev.tfinger.y * WindowHeight;
2842 }
2843 }
2844
2845 break;
2846 #endif
2847
2848 case SDL_QUIT:
2849 quit = true;
2850 break;
2851 }
2852 if( text_refresh && !is_repeat ) {
2853 break;
2854 }
2855 }
2856 if( need_redraw ) {
2857 restore_on_out_of_scope<input_event> prev_last_input( last_input );
2858 // FIXME: SDL_RENDER_TARGETS_RESET only seems to be fired after the first redraw
2859 // when restoring the window after system sleep, rather than immediately
2860 // on focus gain. This seems to mess up the first redraw and
2861 // causes black screen that lasts ~0.5 seconds before the screen
2862 // contents are redrawn in the following code.
2863 window_dimensions dim = get_window_dimensions( catacurses::stdscr );
2864 ui_manager::invalidate( rectangle<point>( point_zero, dim.window_size_pixel ), false );
2865 ui_manager::redraw();
2866 }
2867 if( needupdate ) {
2868 try_sdl_update();
2869 }
2870 if( quit ) {
2871 catacurses::endwin();
2872 exit( 0 );
2873 }
2874 }
2875
2876 //***********************************
2877 //Pseudo-Curses Functions *
2878 //***********************************
2879
2880 // Calculates the new width of the window
projected_window_width()2881 int projected_window_width()
2882 {
2883 return get_option<int>( "TERMINAL_X" ) * fontwidth;
2884 }
2885
2886 // Calculates the new height of the window
projected_window_height()2887 int projected_window_height()
2888 {
2889 return get_option<int>( "TERMINAL_Y" ) * fontheight;
2890 }
2891
init_term_size_and_scaling_factor()2892 static void init_term_size_and_scaling_factor()
2893 {
2894 scaling_factor = 1;
2895 point terminal( get_option<int>( "TERMINAL_X" ), get_option<int>( "TERMINAL_Y" ) );
2896
2897 #if !defined(__ANDROID__)
2898
2899 if( get_option<std::string>( "SCALING_FACTOR" ) == "2" ) {
2900 scaling_factor = 2;
2901 } else if( get_option<std::string>( "SCALING_FACTOR" ) == "4" ) {
2902 scaling_factor = 4;
2903 }
2904
2905 if( scaling_factor > 1 ) {
2906
2907 int max_width, max_height;
2908
2909 int current_display_id = std::stoi( get_option<std::string>( "DISPLAY" ) );
2910 SDL_DisplayMode current_display;
2911
2912 if( SDL_GetDesktopDisplayMode( current_display_id, ¤t_display ) == 0 ) {
2913 if( get_option<std::string>( "FULLSCREEN" ) == "no" ) {
2914
2915 // Make a maximized test window to determine maximum windowed size
2916 SDL_Window_Ptr test_window;
2917 test_window.reset( SDL_CreateWindow( "test_window",
2918 SDL_WINDOWPOS_CENTERED_DISPLAY( current_display_id ),
2919 SDL_WINDOWPOS_CENTERED_DISPLAY( current_display_id ),
2920 FULL_SCREEN_WIDTH * fontwidth,
2921 FULL_SCREEN_HEIGHT * fontheight,
2922 SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_MAXIMIZED
2923 ) );
2924
2925 SDL_GetWindowSize( test_window.get(), &max_width, &max_height );
2926
2927 // If the video subsystem isn't reset the test window messes things up later
2928 test_window.reset();
2929 SDL_QuitSubSystem( SDL_INIT_VIDEO );
2930 SDL_InitSubSystem( SDL_INIT_VIDEO );
2931
2932 } else {
2933 // For fullscreen or window borderless maximum size is the display size
2934 max_width = current_display.w;
2935 max_height = current_display.h;
2936 }
2937 } else {
2938 dbg( D_WARNING ) << "Failed to get current Display Mode, assuming infinite display size.";
2939 max_width = INT_MAX;
2940 max_height = INT_MAX;
2941 }
2942
2943 if( terminal.x * fontwidth > max_width ||
2944 FULL_SCREEN_WIDTH * fontwidth * scaling_factor > max_width ) {
2945 if( FULL_SCREEN_WIDTH * fontwidth * scaling_factor > max_width ) {
2946 dbg( D_WARNING ) << "SCALING_FACTOR set too high for display size, resetting to 1";
2947 scaling_factor = 1;
2948 terminal.x = max_width / fontwidth;
2949 terminal.y = max_height / fontheight;
2950 get_options().get_option( "SCALING_FACTOR" ).setValue( "1" );
2951 } else {
2952 terminal.x = max_width / fontwidth;
2953 }
2954 }
2955
2956 if( terminal.y * fontheight > max_height ||
2957 FULL_SCREEN_HEIGHT * fontheight * scaling_factor > max_height ) {
2958 if( FULL_SCREEN_HEIGHT * fontheight * scaling_factor > max_height ) {
2959 dbg( D_WARNING ) << "SCALING_FACTOR set too high for display size, resetting to 1";
2960 scaling_factor = 1;
2961 terminal.x = max_width / fontwidth;
2962 terminal.y = max_height / fontheight;
2963 get_options().get_option( "SCALING_FACTOR" ).setValue( "1" );
2964 } else {
2965 terminal.y = max_height / fontheight;
2966 }
2967 }
2968
2969 terminal.x -= terminal.x % scaling_factor;
2970 terminal.y -= terminal.y % scaling_factor;
2971
2972 terminal.x = std::max( FULL_SCREEN_WIDTH * scaling_factor, terminal.x );
2973 terminal.y = std::max( FULL_SCREEN_HEIGHT * scaling_factor, terminal.y );
2974
2975 get_options().get_option( "TERMINAL_X" ).setValue(
2976 std::max( FULL_SCREEN_WIDTH * scaling_factor, terminal.x ) );
2977 get_options().get_option( "TERMINAL_Y" ).setValue(
2978 std::max( FULL_SCREEN_HEIGHT * scaling_factor, terminal.y ) );
2979
2980 get_options().save();
2981 }
2982
2983 #endif //__ANDROID__
2984
2985 TERMINAL_WIDTH = terminal.x / scaling_factor;
2986 TERMINAL_HEIGHT = terminal.y / scaling_factor;
2987 }
2988
2989 //Basic Init, create the font, backbuffer, etc
init_interface()2990 void catacurses::init_interface()
2991 {
2992 last_input = input_event();
2993 inputdelay = -1;
2994
2995 InitSDL();
2996
2997 get_options().init();
2998 get_options().load();
2999 set_language(); //Prevent translated language strings from causing an error if language not set
3000
3001 font_loader fl;
3002 fl.load();
3003 fl.fontwidth = get_option<int>( "FONT_WIDTH" );
3004 fl.fontheight = get_option<int>( "FONT_HEIGHT" );
3005 fl.fontsize = get_option<int>( "FONT_SIZE" );
3006 fl.fontblending = get_option<bool>( "FONT_BLENDING" );
3007 fl.map_fontsize = get_option<int>( "MAP_FONT_SIZE" );
3008 fl.map_fontwidth = get_option<int>( "MAP_FONT_WIDTH" );
3009 fl.map_fontheight = get_option<int>( "MAP_FONT_HEIGHT" );
3010 fl.overmap_fontsize = get_option<int>( "OVERMAP_FONT_SIZE" );
3011 fl.overmap_fontwidth = get_option<int>( "OVERMAP_FONT_WIDTH" );
3012 fl.overmap_fontheight = get_option<int>( "OVERMAP_FONT_HEIGHT" );
3013 ::fontwidth = fl.fontwidth;
3014 ::fontheight = fl.fontheight;
3015
3016 init_term_size_and_scaling_factor();
3017
3018 WinCreate();
3019
3020 dbg( D_INFO ) << "Initializing SDL Tiles context";
3021 tilecontext = std::make_unique<cata_tiles>( renderer, geometry );
3022 try {
3023 tilecontext->load_tileset( get_option<std::string>( "TILES" ), true );
3024 } catch( const std::exception &err ) {
3025 dbg( D_ERROR ) << "failed to check for tileset: " << err.what();
3026 // use_tiles is the cached value of the USE_TILES option.
3027 // most (all?) code refers to this to see if cata_tiles should be used.
3028 // Setting it to false disables this from getting used.
3029 use_tiles = false;
3030 }
3031
3032 color_loader<SDL_Color>().load( windowsPalette );
3033 init_colors();
3034
3035 // initialize sound set
3036 load_soundset();
3037
3038 font = std::make_unique<FontFallbackList>( renderer, format, fl.fontwidth, fl.fontheight,
3039 windowsPalette, fl.typeface, fl.fontsize, fl.fontblending );
3040 map_font = std::make_unique<FontFallbackList>( renderer, format, fl.map_fontwidth,
3041 fl.map_fontheight,
3042 windowsPalette, fl.map_typeface, fl.map_fontsize, fl.fontblending );
3043 overmap_font = std::make_unique<FontFallbackList>( renderer, format, fl.overmap_fontwidth,
3044 fl.overmap_fontheight,
3045 windowsPalette, fl.overmap_typeface, fl.overmap_fontsize, fl.fontblending );
3046 stdscr = newwin( get_terminal_height(), get_terminal_width(), point_zero );
3047 //newwin calls `new WINDOW`, and that will throw, but not return nullptr.
3048
3049 #if defined(__ANDROID__)
3050 // Make sure we initialize preview_terminal_width/height to sensible values
3051 preview_terminal_width = TERMINAL_WIDTH * fontwidth;
3052 preview_terminal_height = TERMINAL_HEIGHT * fontheight;
3053 #endif
3054 }
3055
3056 // This is supposed to be called from init.cpp, and only from there.
load_tileset()3057 void load_tileset()
3058 {
3059 if( !tilecontext || !use_tiles ) {
3060 return;
3061 }
3062 tilecontext->load_tileset( get_option<std::string>( "TILES" ) );
3063 tilecontext->do_tile_loading_report();
3064 }
3065
3066 //Ends the terminal, destroy everything
endwin()3067 void catacurses::endwin()
3068 {
3069 tilecontext.reset();
3070 font.reset();
3071 map_font.reset();
3072 overmap_font.reset();
3073 WinDestroy();
3074 }
3075
3076 template<>
from_rgb(const int r,const int g,const int b)3077 SDL_Color color_loader<SDL_Color>::from_rgb( const int r, const int g, const int b )
3078 {
3079 SDL_Color result;
3080 result.b = b; //Blue
3081 result.g = g; //Green
3082 result.r = r; //Red
3083 result.a = 0xFF; // Opaque
3084 return result;
3085 }
3086
set_timeout(const int t)3087 void input_manager::set_timeout( const int t )
3088 {
3089 input_timeout = t;
3090 inputdelay = t;
3091 }
3092
3093 // This is how we're actually going to handle input events, SDL getch
3094 // is simply a wrapper around this.
get_input_event(const keyboard_mode preferred_keyboard_mode)3095 input_event input_manager::get_input_event( const keyboard_mode preferred_keyboard_mode )
3096 {
3097 if( test_mode ) {
3098 // input should be skipped in caller's code
3099 throw std::runtime_error( "input_manager::get_input_event called in test mode" );
3100 }
3101
3102 #if !defined(__ANDROID__) && !defined(TARGET_OS_IPHONE)
3103 if( actual_keyboard_mode( preferred_keyboard_mode ) == keyboard_mode::keychar ) {
3104 SDL_StartTextInput();
3105 } else {
3106 SDL_StopTextInput();
3107 }
3108 #else
3109 // TODO: support keycode mode if hardware keyboard is connected?
3110 ( void ) preferred_keyboard_mode;
3111 #endif
3112
3113 previously_pressed_key = 0;
3114 // standards note: getch is sometimes required to call refresh
3115 // see, e.g., http://linux.die.net/man/3/getch
3116 // so although it's non-obvious, that refresh() call (and maybe InvalidateRect?) IS supposed to be there
3117
3118 wrefresh( catacurses::stdscr );
3119
3120 if( inputdelay < 0 ) {
3121 do {
3122 CheckMessages();
3123 if( last_input.type != input_event_t::error ) {
3124 break;
3125 }
3126 SDL_Delay( 1 );
3127 } while( last_input.type == input_event_t::error );
3128 } else if( inputdelay > 0 ) {
3129 uint32_t starttime = SDL_GetTicks();
3130 uint32_t endtime = 0;
3131 bool timedout = false;
3132 do {
3133 CheckMessages();
3134 endtime = SDL_GetTicks();
3135 if( last_input.type != input_event_t::error ) {
3136 break;
3137 }
3138 SDL_Delay( 1 );
3139 timedout = endtime >= starttime + inputdelay;
3140 if( timedout ) {
3141 last_input.type = input_event_t::timeout;
3142 }
3143 } while( !timedout );
3144 } else {
3145 CheckMessages();
3146 }
3147
3148 if( last_input.type == input_event_t::mouse ) {
3149 SDL_GetMouseState( &last_input.mouse_pos.x, &last_input.mouse_pos.y );
3150 } else if( last_input.type == input_event_t::keyboard_char ) {
3151 previously_pressed_key = last_input.get_first_input();
3152 #if defined(__ANDROID__)
3153 android_vibrate();
3154 #endif
3155 }
3156 #if defined(__ANDROID__)
3157 else if( last_input.type == input_event_t::gamepad ) {
3158 android_vibrate();
3159 }
3160 #endif
3161
3162 return last_input;
3163 }
3164
gamepad_available()3165 bool gamepad_available()
3166 {
3167 return joystick != nullptr;
3168 }
3169
rescale_tileset(int size)3170 void rescale_tileset( int size )
3171 {
3172 tilecontext->set_draw_scale( size );
3173 g->mark_main_ui_adaptor_resize();
3174 }
3175
get_window_dimensions(const catacurses::window & win,const point & pos,const point & size)3176 static window_dimensions get_window_dimensions( const catacurses::window &win,
3177 const point &pos, const point &size )
3178 {
3179 window_dimensions dim;
3180 if( use_tiles && g && win == g->w_terrain ) {
3181 // tiles might have different dimensions than standard font
3182 dim.scaled_font_size.x = tilecontext->get_tile_width();
3183 dim.scaled_font_size.y = tilecontext->get_tile_height();
3184 } else if( map_font && g && win == g->w_terrain ) {
3185 // map font (if any) might differ from standard font
3186 dim.scaled_font_size.x = map_font->width;
3187 dim.scaled_font_size.y = map_font->height;
3188 } else if( overmap_font && g && win == g->w_overmap ) {
3189 dim.scaled_font_size.x = overmap_font->width;
3190 dim.scaled_font_size.y = overmap_font->height;
3191 } else {
3192 dim.scaled_font_size.x = fontwidth;
3193 dim.scaled_font_size.y = fontheight;
3194 }
3195
3196 // multiplied by the user's specified scaling factor regardless of whether tiles are in use
3197 dim.scaled_font_size *= get_scaling_factor();
3198
3199 if( win ) {
3200 cata_cursesport::WINDOW *const pwin = win.get<cata_cursesport::WINDOW>();
3201 dim.window_pos_cell = pwin->pos;
3202 dim.window_size_cell.x = pwin->width;
3203 dim.window_size_cell.y = pwin->height;
3204 } else {
3205 dim.window_pos_cell = pos;
3206 dim.window_size_cell = size;
3207 }
3208
3209 // the window position is *always* in standard font dimensions!
3210 dim.window_pos_pixel = point( dim.window_pos_cell.x * fontwidth,
3211 dim.window_pos_cell.y * fontheight );
3212 // But the size of the window is in the font dimensions of the window.
3213 dim.window_size_pixel.x = dim.window_size_cell.x * dim.scaled_font_size.x;
3214 dim.window_size_pixel.y = dim.window_size_cell.y * dim.scaled_font_size.y;
3215
3216 return dim;
3217 }
3218
get_window_dimensions(const catacurses::window & win)3219 window_dimensions get_window_dimensions( const catacurses::window &win )
3220 {
3221 return get_window_dimensions( win, point_zero, point_zero );
3222 }
3223
get_window_dimensions(const point & pos,const point & size)3224 window_dimensions get_window_dimensions( const point &pos, const point &size )
3225 {
3226 return get_window_dimensions( {}, pos, size );
3227 }
3228
get_coordinates(const catacurses::window & capture_win_)3229 cata::optional<tripoint> input_context::get_coordinates( const catacurses::window &capture_win_ )
3230 {
3231 if( !coordinate_input_received ) {
3232 return cata::nullopt;
3233 }
3234
3235 const catacurses::window &capture_win = capture_win_ ? capture_win_ : g->w_terrain;
3236 const window_dimensions dim = get_window_dimensions( capture_win );
3237
3238 const int &fw = dim.scaled_font_size.x;
3239 const int &fh = dim.scaled_font_size.y;
3240 const point &win_min = dim.window_pos_pixel;
3241 const point &win_size = dim.window_size_pixel;
3242 const point win_max = win_min + win_size;
3243
3244 // Translate mouse coordinates to map coordinates based on tile size
3245 // Check if click is within bounds of the window we care about
3246 const inclusive_rectangle<point> win_bounds( win_min, win_max );
3247 if( !win_bounds.contains( coordinate ) ) {
3248 return cata::nullopt;
3249 }
3250
3251 point view_offset;
3252 if( capture_win == g->w_terrain ) {
3253 view_offset = g->ter_view_p.xy();
3254 }
3255
3256 const point screen_pos = coordinate - win_min;
3257 point p;
3258 if( tile_iso && use_tiles ) {
3259 const float win_mid_x = win_min.x + win_size.x / 2.0f;
3260 const float win_mid_y = -win_min.y + win_size.y / 2.0f;
3261 const int screen_col = std::round( ( screen_pos.x - win_mid_x ) / ( fw / 2.0 ) );
3262 const int screen_row = std::round( ( screen_pos.y - win_mid_y ) / ( fw / 4.0 ) );
3263 const point selected( ( screen_col - screen_row ) / 2, ( screen_row + screen_col ) / 2 );
3264 p = view_offset + selected;
3265 } else {
3266 const point selected( screen_pos.x / fw, screen_pos.y / fh );
3267 p = view_offset + selected - dim.window_size_cell / 2;
3268 }
3269
3270 return tripoint( p, get_map().get_abs_sub().z );
3271 }
3272
get_terminal_width()3273 int get_terminal_width()
3274 {
3275 return TERMINAL_WIDTH;
3276 }
3277
get_terminal_height()3278 int get_terminal_height()
3279 {
3280 return TERMINAL_HEIGHT;
3281 }
3282
get_scaling_factor()3283 int get_scaling_factor()
3284 {
3285 return scaling_factor;
3286 }
3287
map_font_width()3288 static int map_font_width()
3289 {
3290 if( use_tiles && tilecontext ) {
3291 return tilecontext->get_tile_width();
3292 }
3293 return ( map_font ? map_font.get() : font.get() )->width;
3294 }
3295
map_font_height()3296 static int map_font_height()
3297 {
3298 if( use_tiles && tilecontext ) {
3299 return tilecontext->get_tile_height();
3300 }
3301 return ( map_font ? map_font.get() : font.get() )->height;
3302 }
3303
overmap_font_width()3304 static int overmap_font_width()
3305 {
3306 return ( overmap_font ? overmap_font.get() : font.get() )->width;
3307 }
3308
overmap_font_height()3309 static int overmap_font_height()
3310 {
3311 return ( overmap_font ? overmap_font.get() : font.get() )->height;
3312 }
3313
to_map_font_dim_width(int & w)3314 void to_map_font_dim_width( int &w )
3315 {
3316 w = ( w * fontwidth ) / map_font_width();
3317 }
3318
to_map_font_dim_height(int & h)3319 void to_map_font_dim_height( int &h )
3320 {
3321 h = ( h * fontheight ) / map_font_height();
3322 }
3323
to_map_font_dimension(int & w,int & h)3324 void to_map_font_dimension( int &w, int &h )
3325 {
3326 to_map_font_dim_width( w );
3327 to_map_font_dim_height( h );
3328 }
3329
from_map_font_dimension(int & w,int & h)3330 void from_map_font_dimension( int &w, int &h )
3331 {
3332 w = ( w * map_font_width() + fontwidth - 1 ) / fontwidth;
3333 h = ( h * map_font_height() + fontheight - 1 ) / fontheight;
3334 }
3335
to_overmap_font_dimension(int & w,int & h)3336 void to_overmap_font_dimension( int &w, int &h )
3337 {
3338 w = ( w * fontwidth ) / overmap_font_width();
3339 h = ( h * fontheight ) / overmap_font_height();
3340 }
3341
is_draw_tiles_mode()3342 bool is_draw_tiles_mode()
3343 {
3344 return use_tiles;
3345 }
3346
3347 /** Saves a screenshot of the current viewport, as a PNG file, to the given location.
3348 * @param file_path: A full path to the file where the screenshot should be saved.
3349 * @returns `true` if the screenshot generation was successful, `false` otherwise.
3350 */
save_screenshot(const std::string & file_path)3351 bool save_screenshot( const std::string &file_path )
3352 {
3353 // Note: the viewport is returned by SDL and we don't have to manage its lifetime.
3354 SDL_Rect viewport;
3355
3356 // Get the viewport size (width and height of the screen)
3357 SDL_RenderGetViewport( renderer.get(), &viewport );
3358
3359 // Create SDL_Surface with depth of 32 bits (note: using zeros for the RGB masks sets a default value, based on the depth; Alpha mask will be 0).
3360 SDL_Surface_Ptr surface = CreateRGBSurface( 0, viewport.w, viewport.h, 32, 0, 0, 0, 0 );
3361
3362 // Get data from SDL_Renderer and save them into surface
3363 if( printErrorIf( SDL_RenderReadPixels( renderer.get(), nullptr, surface->format->format,
3364 surface->pixels, surface->pitch ) != 0,
3365 "save_screenshot: cannot read data from SDL_Renderer." ) ) {
3366 return false;
3367 }
3368
3369 // Save screenshot as PNG file
3370 if( printErrorIf( IMG_SavePNG( surface.get(), file_path.c_str() ) != 0,
3371 std::string( "save_screenshot: cannot save screenshot file: " + file_path ).c_str() ) ) {
3372 return false;
3373 }
3374
3375 return true;
3376 }
3377
3378 #ifdef _WIN32
getWindowHandle()3379 HWND getWindowHandle()
3380 {
3381 SDL_SysWMinfo info;
3382 SDL_VERSION( &info.version );
3383 SDL_GetWindowWMInfo( ::window.get(), &info );
3384 return info.info.win.window;
3385 }
3386 #endif
3387
3388 #endif // TILES
3389
window_contains_point_relative(const catacurses::window & win,const point & p)3390 bool window_contains_point_relative( const catacurses::window &win, const point &p )
3391 {
3392 const point bound = point( catacurses::getmaxx( win ), catacurses::getmaxy( win ) );
3393 const half_open_rectangle<point> win_bounds( point_zero, bound );
3394 return win_bounds.contains( p );
3395 }
3396