/* 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 { // Touch button type: tap, stick or slider touchButtonType type; // Field of button float x1, y1, x2, y2; // Button texture int texture; rgba_t color; char texturefile[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 texturefile[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 ) touchdefaultbutton_t g_DefaultButtons[256]; int g_LastDefaultButton; static CVAR_DEFINE_AUTO( touch_in_menu, "0", FCVAR_FILTERABLE, "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"); CVAR_DEFINE_AUTO( touch_enable, DEFAULT_TOUCH_ENABLE, FCVAR_ARCHIVE | FCVAR_FILTERABLE, "enable touch controls" ); CVAR_DEFINE_AUTO( touch_emulate, "0", FCVAR_ARCHIVE | FCVAR_FILTERABLE, "emulate touch with mouse" ); // code looks smaller with it #define B(x) (button->x) #define SCR_W ((float)refState.width) #define SCR_H ((float)refState.height) #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 = SCR_H/SCR_W; 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; else if( touch.actual_aspect_ratio ) return touch.actual_aspect_ratio; else if( refState.width && refState.height ) return SCR_H/SCR_W; else 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 returns 0 on success, non-zero on error ========================== */ static inline int Touch_ExportButtonToConfig( file_t *f, touch_button_t *button, qboolean keepAspect ) { string newCommand; int flags = button->flags; if( FBitSet( flags, TOUCH_FL_CLIENT )) return 1; // 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, B( command ), sizeof( newCommand )); FS_Printf( f, "touch_addbutton \"%s\" \"%s\" \"%s\" %f %f %f %f %d %d %d %d %d", B(name), B(texturefile), newCommand, B(x1), B(y1), B(x2), B(y2), B(color[0]), B(color[1]), B(color[2]), B(color[3]), flags ); if( keepAspect ) { float aspect = ( B(y2) - B(y1) ) / ( ( B(x2) - B(x1) ) /(Touch_AspectRatio()) ); FS_Printf( f, " %f\n", aspect ); } else FS_Printf( f, "\n" ); return 0; } /* ================= Touch_DumpConfig Dump config to file ================= */ static qboolean Touch_DumpConfig( const char *name, const char *profilename ) { file_t *f; 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, "//\tCopyright FWGS & XashXT group %s (c)\n", Q_timestamp( TIME_YEAR_ONLY )); 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 \"%f\"\n", touch_pitch.value ); FS_Printf( f, "touch_yaw \"%f\"\n", touch_yaw.value ); FS_Printf( f, "touch_forwardzone \"%f\"\n", touch_forwardzone.value ); FS_Printf( f, "touch_sidezone \"%f\"\n", touch_sidezone.value ); FS_Printf( f, "touch_nonlinear_look \"%d\"\n", touch_nonlinear_look.value ? 1 : 0 ); FS_Printf( f, "touch_pow_factor \"%f\"\n", touch_pow_factor.value ); FS_Printf( f, "touch_pow_mult \"%f\"\n", touch_pow_mult.value ); FS_Printf( f, "touch_exp_mult \"%f\"\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 \"%f\"\n", touch_highlight_r.value ); FS_Printf( f, "touch_highlight_g \"%f\"\n", touch_highlight_g.value ); FS_Printf( f, "touch_highlight_b \"%f\"\n", touch_highlight_b.value ); FS_Printf( f, "touch_highlight_a \"%f\"\n", touch_highlight_a.value ); FS_Printf( f, "\n// _joy and _dpad options\n" ); FS_Printf( f, "touch_dpad_radius \"%f\"\n", touch_dpad_radius.value ); FS_Printf( f, "touch_joy_radius \"%f\"\n", touch_joy_radius.value ); FS_Printf( f, "\n// how much slowdown when Precise Look button pressed\n" ); FS_Printf( f, "touch_precise_amount \"%f\"\n", touch_precise_amount.value ); FS_Printf( f, "\n// enable/disable move indicator\n" ); FS_Printf( f, "touch_move_indicator \"%f\"\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 %f\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 ) { file_t *f; string newconfigfile, oldconfigfile; if( !touch.list_user.first ) return; if( Sys_CheckParm( "-nowriteconfig" ) || !touch.configchanged || !touch.config_loaded ) return; Con_DPrintf( "Touch_WriteConfig(): %s\n", 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, profilebase; if( Cmd_Argc() != 2 ) { Con_Printf( S_USAGE "touch_exportconfig \n" ); return; } if( !touch.list_user.first ) return; name = Cmd_Argv( 1 ); if( Q_strstr( name, "touch_presets/" )) { 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 ) { touch_button_t *button; rgba_t c = {0,0,0,0}; if( !touch.list_user.first ) return; for( button = touch.list_user.first; button; button = button->next ) { float aspect; int flags = button->flags; 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 = ( B(y2) - B(y1) ) / ( ( B(x2) - B(x1) ) /(Touch_AspectRatio()) ); if( memcmp( &c, &B(color), sizeof( rgba_t ) ) ) { Con_Printf( "unsigned char color[] = { %d, %d, %d, %d };\n", B(color[0]), B(color[1]), B(color[2]), B(color[3]) ); memcpy( &c, &B(color), sizeof( rgba_t ) ); } Con_Printf( "TOUCH_ADDDEFAULT( \"%s\", \"%s\", \"%s\", %f, %f, %f, %f, color, %d, %f, %d );\n", B(name), B(texturefile), B(command), B(x1), B(y1), B(x2), B(y2), (B(type) == touch_command)?(fabs( aspect - 1.0f) < 0.0001)?2:1:0, 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( &B(x1), &B(y1), &B(x2), &B(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 %f %f %f %f %d %d %d %d %d\n", B(name), B(texturefile), B(command), B(x1), B(y1), B(x2), B(y2), B(color[0]), B(color[1]), B(color[2]), B(color[3]), B(flags) ); if( B(flags) & TOUCH_FL_CLIENT) continue; UI_AddTouchButtonToList( B(name), B(texturefile), B(command),B(color), B(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_FindButton( touchbuttonlist_t *list, const char *name, qboolean privileged ) { touch_button_t *button; for( button = list->first; button; button = button->next ) { if( !privileged && !FBitSet( button->flags, TOUCH_FL_UNPRIVILEGED )) continue; if( Q_strncmp( button->name, name, sizeof( button->name ))) continue; return button; } return NULL; } static touch_button_t *Touch_FindFirst( touchbuttonlist_t *list, const char *name, qboolean privileged ) { touch_button_t *button; for( button = list->first; button; button = button->next ) { if( !privileged && !FBitSet( button->flags, TOUCH_FL_UNPRIVILEGED )) continue; if(( Q_strchr( name, '*' ) && Q_stricmpext( name, button->name )) || !Q_strncmp( name, button->name, sizeof( button->name ))) { return button; } } return NULL; } void Touch_SetClientOnly( byte state ) { // TODO: fix clash with vgui cursors touch.clientonly = state; touch.move_finger = touch.look_finger = -1; touch.forward = touch.side = 0; if( state ) { IN_DeactivateMouse(); } else { 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