/* touch.c - touchscreen support prototype Copyright (C) 2015-2018 mittorn This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. */ #include "common.h" #include "input.h" #include "client.h" #include "math.h" #include "vgui_draw.h" #include "mobility_int.h" typedef enum { touch_command, // just tap a button touch_move, // like a joystick stick touch_joy, // like a joystick stick, centered touch_dpad, // only two directions touch_look, // like a touchpad touch_wheel // scroll-like } touchButtonType; typedef enum { state_none = 0, state_edit, state_edit_move } touchState; typedef enum { round_none = 0, round_grid, round_aspect } touchRound; typedef struct touch_button_s { touchButtonType type; // button coordinates float x1, y1, x2, y2; int gl_texturenum; rgba_t color; char texture[256]; char command[256]; char name[32]; int finger; int flags; float fade; float fadespeed; float fadeend; float aspect; // Double-linked list struct touch_button_s *next; struct touch_button_s *prev; } touch_button_t; typedef struct touchdefaultbutton_s { char name[32]; char texture[256]; char command[256]; float x1, y1, x2, y2; rgba_t color; touchRound round; float aspect; int flags; } touchdefaultbutton_t; typedef struct touchbuttonlist_s { touch_button_t *first; touch_button_t *last; } touchbuttonlist_t; static struct touch_s { qboolean initialized; qboolean config_loaded; touchbuttonlist_t list_user, list_edit; poolhandle_t mempool; touchState state; int look_finger; int move_finger; int wheel_finger; touch_button_t *move_button; float move_start_x; float move_start_y; float wheel_amount; string wheel_up; string wheel_down; string wheel_end; int wheel_count; qboolean wheel_horizontal; float forward; float side; float yaw; float pitch; // editing touch_button_t *edit; touch_button_t *selection; touch_button_t *hidebutton; int resize_finger; qboolean showeditbuttons; // other features qboolean clientonly; rgba_t scolor; int swidth; qboolean precision; // textures int whitetexture; int joytexture; // touch indicator qboolean configchanged; float actual_aspect_ratio; // maximum aspect ratio from launch, or aspect ratio when entering editor float config_aspect_ratio; // aspect ratio set by command from config or after entering editor } touch; // private to the engine flags #define TOUCH_FL_UNPRIVILEGED BIT( 10 ) static touchdefaultbutton_t *g_DefaultButtons; static size_t g_DefaultButtonsLength; static CVAR_DEFINE_AUTO( touch_in_menu, "0", FCVAR_PRIVILEGED, "draw touch in menu (for internal use only)" ); static CVAR_DEFINE_AUTO( touch_forwardzone, "0.06", FCVAR_FILTERABLE, "forward touch zone" ); static CVAR_DEFINE_AUTO( touch_sidezone, "0.06", FCVAR_FILTERABLE, "side touch zone" ); static CVAR_DEFINE_AUTO( touch_pitch, "90", FCVAR_FILTERABLE, "touch pitch sensitivity" ); static CVAR_DEFINE_AUTO( touch_yaw, "120", FCVAR_FILTERABLE, "touch yaw sensitivity" ); static CVAR_DEFINE_AUTO( touch_nonlinear_look, "0", FCVAR_FILTERABLE, "enable nonlinear touch look" ); static CVAR_DEFINE_AUTO( touch_pow_factor, "1.3", FCVAR_FILTERABLE, "set > 1 to enable" ); static CVAR_DEFINE_AUTO( touch_pow_mult, "400.0", FCVAR_FILTERABLE, "power multiplier, usually 200-1000" ); static CVAR_DEFINE_AUTO( touch_exp_mult, "0", FCVAR_FILTERABLE, "exponent multiplier, usually 20-200, 0 to disable" ); static CVAR_DEFINE_AUTO( touch_grid_count, "50", FCVAR_FILTERABLE, "touch grid count" ); static CVAR_DEFINE_AUTO( touch_grid_enable, "1", FCVAR_FILTERABLE, "enable touch grid" ); static CVAR_DEFINE_AUTO( touch_config_file, "touch.cfg", FCVAR_ARCHIVE | FCVAR_PRIVILEGED, "current touch profile file" ); static CVAR_DEFINE_AUTO( touch_precise_amount, "0.5", FCVAR_FILTERABLE, "sensitivity multiplier for precise-look" ); static CVAR_DEFINE_AUTO( touch_highlight_r, "1.0", 0, "highlight r color" ); static CVAR_DEFINE_AUTO( touch_highlight_g, "1.0", 0, "highlight g color" ); static CVAR_DEFINE_AUTO( touch_highlight_b, "1.0", 0, "highlight b color" ); static CVAR_DEFINE_AUTO( touch_highlight_a, "1.0", 0, "highlight alpha" ); static CVAR_DEFINE_AUTO( touch_dpad_radius, "1.0", FCVAR_FILTERABLE, "dpad radius multiplier" ); static CVAR_DEFINE_AUTO( touch_joy_radius, "1.0", FCVAR_FILTERABLE, "joy radius multiplier" ); static CVAR_DEFINE_AUTO( touch_move_indicator, "0.0", FCVAR_FILTERABLE, "indicate move events (0 to disable)" ); static CVAR_DEFINE_AUTO( touch_joy_texture, "touch_default/joy", FCVAR_FILTERABLE, "texture for move indicator"); static CVAR_DEFINE( touch_emulate, "_touch_emulate", "0", FCVAR_PRIVILEGED, "emulate touch with mouse" ); CVAR_DEFINE_AUTO( touch_enable, DEFAULT_TOUCH_ENABLE, FCVAR_ARCHIVE | FCVAR_FILTERABLE, "enable touch controls" ); // code looks smaller with it #define TO_SCRN_Y(x) (refState.width * (x) * Touch_AspectRatio()) #define TO_SCRN_X(x) (refState.width * (x)) static void IN_TouchCheckCoords( float *x1, float *y1, float *x2, float *y2 ); static void IN_TouchEditClear( void ); static void Touch_InitConfig( void ); void Touch_NotifyResize( void ) { if( refState.width && refState.height && ( !touch.configchanged || !touch.actual_aspect_ratio )) { float aspect_ratio = (float)refState.height / refState.width; if( aspect_ratio < 0.99 && aspect_ratio > touch.actual_aspect_ratio ) touch.actual_aspect_ratio = aspect_ratio; } } static inline float Touch_AspectRatio( void ) { if( touch.config_aspect_ratio ) return touch.config_aspect_ratio; if( touch.actual_aspect_ratio ) return touch.actual_aspect_ratio; if( refState.width && refState.height ) return (float)refState.height / refState.width; return 9.0f / 16.0f; } static void Touch_ConfigAspectRatio_f( void ) { touch.config_aspect_ratio = Q_atof( Cmd_Argv( 1 )); } /* ========================== Touch_ExportButtonToConfig writes button data to config ========================== */ static void Touch_ExportButtonToConfig( file_t *f, const touch_button_t *button, qboolean keepAspect ) { string newCommand; int flags = button->flags; if( FBitSet( flags, TOUCH_FL_CLIENT )) return; // skip temporary buttons if( FBitSet( flags, TOUCH_FL_DEF_SHOW )) ClearBits( flags, TOUCH_FL_HIDE ); if( FBitSet( flags, TOUCH_FL_DEF_HIDE )) SetBits( flags, TOUCH_FL_HIDE ); Cmd_Escape( newCommand, button->command, sizeof( newCommand )); FS_Printf( f, "touch_addbutton \"%s\" \"%s\" \"%s\" %g %g %g %g %d %d %d %d %d", button->name, button->texture, newCommand, button->x1, button->y1, button->x2, button->y2, button->color[0], button->color[1], button->color[2], button->color[3], flags ); if( keepAspect ) { float aspect = ( button->y2 - button->y1 ) / (( button->x2 - button->x1 ) / Touch_AspectRatio( )); FS_Printf( f, " %g\n", aspect ); } else FS_Printf( f, "\n" ); } /* ================= Touch_DumpConfig Dump config to file ================= */ static qboolean Touch_DumpConfig( const char *name, const char *profilename ) { file_t *f; const touch_button_t *button; f = FS_Open( name, "w", true ); if( !f ) { Con_Printf( S_ERROR "Couldn't write %s.\n", name ); return false; } FS_Printf( f, "//=======================================================================\n"); FS_Printf( f, "//\tGenerated by "XASH_ENGINE_NAME" (%i, %s, %s, %s-%s)\n", Q_buildnum(), g_buildcommit, g_buildbranch, Q_buildos(), Q_buildarch()); FS_Printf( f, "//\t\t\ttouchscreen config\n" ); FS_Printf( f, "//=======================================================================\n" ); FS_Printf( f, "\ntouch_config_file \"%s\"\n", profilename ); FS_Printf( f, "\n// touch cvars\n" ); FS_Printf( f, "\n// sensitivity settings\n" ); FS_Printf( f, "touch_pitch \"%g\"\n", touch_pitch.value ); FS_Printf( f, "touch_yaw \"%g\"\n", touch_yaw.value ); FS_Printf( f, "touch_forwardzone \"%g\"\n", touch_forwardzone.value ); FS_Printf( f, "touch_sidezone \"%g\"\n", touch_sidezone.value ); FS_Printf( f, "touch_nonlinear_look \"%d\"\n", touch_nonlinear_look.value ? 1 : 0 ); FS_Printf( f, "touch_pow_factor \"%g\"\n", touch_pow_factor.value ); FS_Printf( f, "touch_pow_mult \"%g\"\n", touch_pow_mult.value ); FS_Printf( f, "touch_exp_mult \"%g\"\n", touch_exp_mult.value ); FS_Printf( f, "\n// grid settings\n" ); FS_Printf( f, "touch_grid_count \"%d\"\n", (int)touch_grid_count.value ); FS_Printf( f, "touch_grid_enable \"%d\"\n", touch_grid_enable.value ? 1 : 0 ); FS_Printf( f, "\n// global overstroke (width, r, g, b, a)\n" ); FS_Printf( f, "touch_set_stroke %d %d %d %d %d\n", touch.swidth, touch.scolor[0], touch.scolor[1], touch.scolor[2], touch.scolor[3] ); FS_Printf( f, "\n// highlight when pressed\n" ); FS_Printf( f, "touch_highlight_r \"%g\"\n", touch_highlight_r.value ); FS_Printf( f, "touch_highlight_g \"%g\"\n", touch_highlight_g.value ); FS_Printf( f, "touch_highlight_b \"%g\"\n", touch_highlight_b.value ); FS_Printf( f, "touch_highlight_a \"%g\"\n", touch_highlight_a.value ); FS_Printf( f, "\n// _joy and _dpad options\n" ); FS_Printf( f, "touch_dpad_radius \"%g\"\n", touch_dpad_radius.value ); FS_Printf( f, "touch_joy_radius \"%g\"\n", touch_joy_radius.value ); FS_Printf( f, "\n// how much slowdown when Precise Look button pressed\n" ); FS_Printf( f, "touch_precise_amount \"%g\"\n", touch_precise_amount.value ); FS_Printf( f, "\n// enable/disable move indicator\n" ); FS_Printf( f, "touch_move_indicator \"%g\"\n", touch_move_indicator.value ); FS_Printf( f, "\n// reset menu state when execing config\n" ); FS_Printf( f, "touch_setclientonly 0\n" ); FS_Printf( f, "\n// touch buttons\n" ); FS_Printf( f, "touch_removeall\n" ); FS_Printf( f, "touch_aspectratio %g\n", Touch_AspectRatio()); for( button = touch.list_user.first; button; button = button->next ) Touch_ExportButtonToConfig( f, button, false ); FS_Close( f ); return true; } /* ================= Touch_WriteConfig save current touch configuration ================= */ void Touch_WriteConfig( void ) { string newconfigfile, oldconfigfile; if( !touch.list_user.first ) return; if( Sys_CheckParm( "-nowriteconfig" ) || !touch.configchanged || !touch.config_loaded ) return; Con_DPrintf( "%s: %s\n", __func__, touch_config_file.string ); Q_snprintf( newconfigfile, sizeof( newconfigfile ), "%s.new", touch_config_file.string ); Q_snprintf( oldconfigfile, sizeof( oldconfigfile ), "%s.bak", touch_config_file.string ); if( Touch_DumpConfig( newconfigfile, touch_config_file.string )) { FS_Delete( oldconfigfile ); FS_Rename( touch_config_file.string, oldconfigfile ); FS_Delete( touch_config_file.string ); FS_Rename( newconfigfile, touch_config_file.string ); } } /* ================= Touch_ExportConfig_f export current touch configuration into profile ================= */ static void Touch_ExportConfig_f( void ) { const char *name; string profilename; if( Cmd_Argc() != 2 ) { Con_Printf( S_USAGE "touch_exportconfig \n" ); return; } if( !touch.list_user.first ) { Con_Printf( "%s: nothing to export\n", __func__ ); return; } name = Cmd_Argv( 1 ); if( Q_strstr( name, "touch_presets/" )) { string profilebase; COM_FileBase( name, profilebase, sizeof( profilebase )); Q_snprintf( profilename, sizeof( profilebase ), "touch_profiles/%s (copy).cfg", profilebase ); } else Q_strncpy( profilename, name, sizeof( profilename )); Con_Reportf( "Exporting config to \"%s\", profile name \"%s\"\n", name, profilename ); Touch_DumpConfig( name, profilename ); } /* ================= Touch_GenerateCode_f export current touch configuration into C code ================= */ static void Touch_GenerateCode_f( void ) { const touch_button_t *button; rgba_t c = { 0 }; if( !touch.list_user.first ) { Con_Printf( "%s: nothing to export\n", __func__ ); return; } for( button = touch.list_user.first; button; button = button->next ) { float aspect; int flags = button->flags; int round; if( FBitSet( flags, TOUCH_FL_CLIENT )) continue; // skip temporary buttons if( FBitSet( flags, TOUCH_FL_DEF_SHOW )) ClearBits( flags, TOUCH_FL_HIDE ); if( FBitSet( flags, TOUCH_FL_DEF_HIDE )) SetBits( flags, TOUCH_FL_HIDE ); aspect = ( button->y2 - button->y1 ) / (( button->x2 - button->x1 ) / Touch_AspectRatio( )); if( memcmp( c, button->color, sizeof( c ))) { Con_Printf( "unsigned char color[] = { %d, %d, %d, %d };\n", button->color[0], button->color[1], button->color[2], button->color[3] ); memcpy( c, button->color, sizeof( c )); } if( button->type == touch_command ) { if( fabs( aspect - 1.0f ) < 0.001 ) round = round_aspect; else round = round_grid; } else round = round_none; Con_Printf( "TOUCH_ADDDEFAULT( \"%s\", \"%s\", \"%s\", %gf, %gf, %gf, %gf, color, %d, %g, %d );\n", button->name, button->texture, button->command, button->x1, button->y1, button->x2, button->y2, round, aspect, flags ); } } static void Touch_RoundAll_f( void ) { touch_button_t *button; if( !touch_grid_enable.value ) return; for( button = touch.list_user.first; button; button = button->next ) IN_TouchCheckCoords( &button->x1, &button->y1, &button->x2, &button->y2 ); } static void Touch_ListButtons_f( void ) { touch_button_t *button; Touch_InitConfig(); for( button = touch.list_user.first; button; button = button->next ) { Con_Printf( "%s %s %s %g %g %g %g %d %d %d %d %d\n", button->name, button->texture, button->command, button->x1, button->y1, button->x2, button->y2, button->color[0], button->color[1], button->color[2], button->color[3], button->flags ); if( FBitSet( button->flags, TOUCH_FL_CLIENT )) continue; UI_AddTouchButtonToList( button->name, button->texture, button->command, button->color, button->flags ); } touch.configchanged = true; } static void Touch_Stroke_f( void ) { if( Cmd_Argc() != 6 ) { Con_Printf( S_USAGE "touch_set_stroke \n"); return; } touch.swidth = Q_atoi( Cmd_Argv( 1 ) ); MakeRGBA( touch.scolor, Q_atoi( Cmd_Argv( 2 ) ), Q_atoi( Cmd_Argv( 3 ) ), Q_atoi( Cmd_Argv( 4 ) ), Q_atoi( Cmd_Argv( 5 ) ) ); } static touch_button_t *Touch_FindNextNoPattern( touch_button_t *buttons, const char *name, qboolean privileged ) { touch_button_t *b; for( b = buttons; b; b = b->next ) { if( !privileged && !FBitSet( b->flags, TOUCH_FL_UNPRIVILEGED )) continue; if( !Q_strncmp( b->name, name, sizeof( b->name ))) return b; } return NULL; } static touch_button_t *Touch_FindButtonNoPattern( touchbuttonlist_t *list, const char *name, qboolean privileged ) { return Touch_FindNextNoPattern( list->first, name, privileged ); } static touch_button_t *Touch_FindNext( touch_button_t *buttons, const char *name, qboolean privileged ) { touch_button_t *b; qboolean has_pattern = Q_strchr( name, '*' ) != NULL; if( !has_pattern ) return Touch_FindNextNoPattern( buttons, name, privileged ); for( b = buttons; b; b = b->next ) { if( !privileged && !FBitSet( b->flags, TOUCH_FL_UNPRIVILEGED )) continue; if( Q_stricmpext( name, b->name )) return b; } return NULL; } static touch_button_t *Touch_FindFirst( touchbuttonlist_t *list, const char *name, qboolean privileged ) { return Touch_FindNext( list->first, name, privileged ); } void Touch_SetClientOnly( byte state ) { // TODO: fix clash with vgui cursors touch.clientonly = state; touch.resize_finger = touch.move_finger = touch.look_finger = touch.wheel_finger = -1; touch.forward = touch.side = 0; if( state ) { Platform_SetCursorType( dc_arrow ); IN_DeactivateMouse(); } else { Platform_SetCursorType( dc_none ); IN_ActivateMouse(); } } static void Touch_SetClientOnly_f( void ) { if( Cmd_Argc() != 2 ) { Con_Printf( S_USAGE "touch_setclientonly \n"); return; } Touch_SetClientOnly( Q_atoi( Cmd_Argv( 1 ))); } static void Touch_RemoveButtonFromList( touchbuttonlist_t *list, const char *name, qboolean privileged ) { touch_button_t *button; IN_TouchEditClear(); while(( button = Touch_FindFirst( &touch.list_user, name, privileged ))) { if( button->prev ) button->prev->next = button->next; else list->first = button->next; if( button->next ) button->next->prev = button->prev; else list->last = button->prev; Mem_Free( button ); } } void Touch_RemoveButton( const char *name, qboolean privileged ) { Touch_RemoveButtonFromList( &touch.list_user, name, privileged ); } static void IN_TouchRemoveButton_f( void ) { if( Cmd_Argc() != 2 ) { Con_Printf( S_USAGE "touch_removebutton