/*
===========================================================================
Copyright (C) 2017-2020 Gian 'myT' Schellenbaum
This file is part of Challenge Quake 3 (CNQ3).
Challenge Quake 3 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 2 of the License,
or (at your option) any later version.
Challenge Quake 3 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.
You should have received a copy of the GNU General Public License
along with Challenge Quake 3. If not, see .
===========================================================================
*/
// Linux main loop, event handling, etc. using SDL 2
#include "linux_local.h"
#include
#include
#include "sdl_local.h"
#include
// About in_focusDelay:
// Suppose you have the game focused in windowed mode with the console down,
// open the command window (alt+F2), then press return or escape.
// On my machine, SDL will first send the X11 FocusIn event and *after that*
// the keypress event for escape or return. For oj, it's the reverse...
// In my scenario, clearing key states after reception of the FocusIn event
// won't prevent the application from receiving the undesired keypresses.
static cvar_t* in_noGrab;
static cvar_t* in_focusDelay;
static cvar_t* m_relative;
static qbool sdl_inputActive = qfalse;
static qbool sdl_forceUnmute = qfalse; // overrides s_autoMute
static int sdl_focusTime = INT_MIN; // timestamp of last X11 FocusIn event
static qbool sdl_focused = qtrue; // does the X11 window have the focus?
static const cvarTableItem_t in_cvars[] = {
{ &in_noGrab, "in_noGrab", "0", 0, CVART_BOOL, NULL, NULL, "disables input grabbing" },
{ &in_focusDelay, "in_focusDelay", "5", CVAR_ARCHIVE, CVART_INTEGER, "0", "100", "milli-seconds keypresses are off after window focus" },
{ &m_relative, "m_relative", "1", CVAR_ARCHIVE, CVART_BOOL, NULL, NULL, "enables SDL's relative mouse mode" }
};
static void Minimize_f();
static const cmdTableItem_t in_cmds[] = {
{ "minimize", &Minimize_f, NULL, "minimizes the window" }
};
static qbool sdl_Version_AtLeast( int major, int minor, int patch )
{
SDL_version v;
SDL_GetVersion(&v);
// has to be SDL 2
if (v.major != major)
return qfalse;
if (v.minor < minor)
return qfalse;
if (v.minor > minor)
return qtrue;
return v.patch >= patch;
}
static void Minimize_f()
{
SDL_MinimizeWindow(glimp.window);
}
static int QuakeKeyFromSDLKey( SDL_Keysym key )
{
if (key.scancode == SDL_SCANCODE_GRAVE)
return '`';
const SDL_Keycode sym = key.sym;
// these ranges map directly to ASCII chars
if ((sym >= SDLK_a && sym <= SDLK_z) ||
(sym >= SDLK_0 && sym <= SDLK_9))
return (int)sym;
// F1 to F24
// SDL splits the values 1-12 and 13-24
// the engine splits the values 1-15 and 16-24
switch (sym) {
case SDLK_F1: return K_F1;
case SDLK_F2: return K_F2;
case SDLK_F3: return K_F3;
case SDLK_F4: return K_F4;
case SDLK_F5: return K_F5;
case SDLK_F6: return K_F6;
case SDLK_F7: return K_F7;
case SDLK_F8: return K_F8;
case SDLK_F9: return K_F9;
case SDLK_F10: return K_F10;
case SDLK_F11: return K_F11;
case SDLK_F12: return K_F12;
case SDLK_F13: return K_F13;
case SDLK_F14: return K_F14;
case SDLK_F15: return K_F15;
case SDLK_F16: return K_F16;
case SDLK_F17: return K_F17;
case SDLK_F18: return K_F18;
case SDLK_F19: return K_F19;
case SDLK_F20: return K_F20;
case SDLK_F21: return K_F21;
case SDLK_F22: return K_F22;
case SDLK_F23: return K_F23;
case SDLK_F24: return K_F24;
case SDLK_UP: return K_UPARROW;
case SDLK_DOWN: return K_DOWNARROW;
case SDLK_LEFT: return K_LEFTARROW;
case SDLK_RIGHT: return K_RIGHTARROW;
case SDLK_TAB: return K_TAB;
case SDLK_RETURN: return K_ENTER;
case SDLK_ESCAPE: return K_ESCAPE;
case SDLK_SPACE: return K_SPACE;
case SDLK_BACKSPACE: return K_BACKSPACE;
case SDLK_CAPSLOCK: return K_CAPSLOCK;
case SDLK_LALT: return K_ALT;
case SDLK_RALT: return K_ALT;
case SDLK_LCTRL: return K_CTRL;
case SDLK_RCTRL: return K_CTRL;
case SDLK_LSHIFT: return K_SHIFT;
case SDLK_RSHIFT: return K_SHIFT;
case SDLK_INSERT: return K_INS;
case SDLK_DELETE: return K_DEL;
case SDLK_PAGEDOWN: return K_PGDN;
case SDLK_PAGEUP: return K_PGUP;
case SDLK_HOME: return K_HOME;
case SDLK_END: return K_END;
case SDLK_KP_7: return K_KP_HOME;
case SDLK_KP_8: return K_KP_UPARROW;
case SDLK_KP_9: return K_KP_PGUP;
case SDLK_KP_4: return K_KP_LEFTARROW;
case SDLK_KP_5: return K_KP_5;
case SDLK_KP_6: return K_KP_RIGHTARROW;
case SDLK_KP_1: return K_KP_END;
case SDLK_KP_2: return K_KP_DOWNARROW;
case SDLK_KP_3: return K_KP_PGDN;
case SDLK_KP_ENTER: return K_KP_ENTER;
case SDLK_KP_0: return K_KP_INS;
case SDLK_KP_DECIMAL: return K_KP_DEL;
case SDLK_KP_DIVIDE: return K_KP_SLASH;
case SDLK_KP_MINUS: return K_KP_MINUS;
case SDLK_KP_PLUS: return K_KP_PLUS;
case SDLK_KP_MULTIPLY: return K_KP_STAR;
case SDLK_BACKSLASH: return K_BACKSLASH;
case SDLK_PAUSE: return K_PAUSE;
case SDLK_NUMLOCKCLEAR: return K_KP_NUMLOCK;
case SDLK_KP_EQUALS: return K_KP_EQUALS;
case SDLK_MENU: return K_MENU;
case SDLK_PERIOD: return '.';
case SDLK_COMMA: return ',';
case SDLK_EXCLAIM: return '!';
case SDLK_HASH: return '#';
case SDLK_PERCENT: return '%';
case SDLK_DOLLAR: return '$';
case SDLK_AMPERSAND: return '&';
case SDLK_QUOTE: return '\'';
case SDLK_LEFTPAREN: return '(';
case SDLK_RIGHTPAREN: return ')';
case SDLK_ASTERISK: return '*';
case SDLK_PLUS: return '+';
case SDLK_MINUS: return '-';
case SDLK_SLASH: return '/';
case SDLK_COLON: return ':';
case SDLK_LESS: return '<';
case SDLK_EQUALS: return '=';
case SDLK_GREATER: return '>';
case SDLK_QUESTION: return '?';
case SDLK_AT: return '@';
case SDLK_LEFTBRACKET: return '[';
case SDLK_RIGHTBRACKET: return ']';
case SDLK_UNDERSCORE: return '_';
case SDLK_SEMICOLON: return ';';
// not handled:
// K_COMMAND (Apple)
// K_POWER (Apple)
// K_AUX1-16
// K_WIN
default: break;
}
if (sym >= 32 && sym <= 126)
return (int)sym;
return -1;
}
static void sdl_Key( const SDL_KeyboardEvent* event, qbool down )
{
const int key = QuakeKeyFromSDLKey(event->keysym);
if (key >= 0)
Lin_QueEvent(Sys_Milliseconds(), SE_KEY, key, down, 0, NULL);
if (down && key == K_BACKSPACE)
Lin_QueEvent(Sys_Milliseconds(), SE_CHAR, 8, 0, 0, NULL);
// ctrl+v
if (down && key == 'v' && (event->keysym.mod & KMOD_CTRL) != 0)
Lin_QueEvent(Sys_Milliseconds(), SE_CHAR, 22, 0, 0, NULL);
}
static void sdl_Text( const SDL_TextInputEvent* event )
{
// text is UTF-8 encoded but we only care for
// chars that are single-byte encoded
const byte key = (byte)event->text[0];
if (key >= 0 && key <= 0x7F)
Lin_QueEvent(Sys_Milliseconds(), SE_CHAR, (int)key, 0, 0, NULL);
}
static void sdl_MouseMotion( const SDL_MouseMotionEvent* event )
{
if (!sdl_inputActive)
return;
// SDL sometimes sends events with both values set to 0
if ((event->xrel | event->yrel) == 0)
return;
Lin_QueEvent(Sys_Milliseconds(), SE_MOUSE, event->xrel, event->yrel, 0, NULL);
}
static void sdl_MouseButton( const SDL_MouseButtonEvent* event, qbool down )
{
if (!sdl_inputActive && down)
return;
static const int mouseButtonCount = 5;
static const int mouseButtons[mouseButtonCount][2] = {
{ SDL_BUTTON_LEFT, K_MOUSE1 },
{ SDL_BUTTON_RIGHT, K_MOUSE2 },
{ SDL_BUTTON_MIDDLE, K_MOUSE3 },
{ SDL_BUTTON_X1, K_MOUSE4 },
{ SDL_BUTTON_X2, K_MOUSE5 }
};
int button = -1;
for(int i = 0; i < mouseButtonCount; ++i) {
if (event->button == mouseButtons[i][0]) {
button = i;
break;
}
}
if (button < 0)
return;
Lin_QueEvent(Sys_Milliseconds(), SE_KEY, mouseButtons[button][1], down, 0, NULL);
}
static void sdl_MouseWheel( const SDL_MouseWheelEvent* event )
{
if (event->y == 0)
return;
#if SDL_VERSION_ATLEAST(2, 0, 4)
int delta = event->y;
if (sdl_Version_AtLeast(2, 0, 4) &&
event->direction == SDL_MOUSEWHEEL_FLIPPED)
delta = -delta;
#else
const int delta = event->y;
#endif
const int key = (delta < 0) ? K_MWHEELDOWN : K_MWHEELUP;
Lin_QueEvent(Sys_Milliseconds(), SE_KEY, key, qtrue, 0, NULL);
Lin_QueEvent(Sys_Milliseconds(), SE_KEY, key, qfalse, 0, NULL);
}
static void sdl_Window( const SDL_WindowEvent* event )
{
// events of interest:
//SDL_WINDOWEVENT_SHOWN
//SDL_WINDOWEVENT_HIDDEN
//SDL_WINDOWEVENT_RESIZED
//SDL_WINDOWEVENT_SIZE_CHANGED // should prevent this from happening except on creation?
//SDL_WINDOWEVENT_MINIMIZED
//SDL_WINDOWEVENT_MAXIMIZED
//SDL_WINDOWEVENT_RESTORED
//SDL_WINDOWEVENT_ENTER // mouse focus gained
//SDL_WINDOWEVENT_LEAVE // mouse focus lost
//SDL_WINDOWEVENT_FOCUS_GAINED // kb focus gained
//SDL_WINDOWEVENT_FOCUS_LOST // kb focus lost
//SDL_WINDOWEVENT_CLOSE
//SDL_WINDOWEVENT_MOVED
switch (event->event) {
case SDL_WINDOWEVENT_MAXIMIZED:
case SDL_WINDOWEVENT_RESTORED:
case SDL_WINDOWEVENT_RESIZED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
case SDL_WINDOWEVENT_MOVED:
// if this turns out to be too expensive, track movement and
// only call when movement stops
sdl_UpdateMonitorIndexFromWindow();
break;
default:
break;
}
switch (event->event) {
case SDL_WINDOWEVENT_SHOWN:
case SDL_WINDOWEVENT_MAXIMIZED:
case SDL_WINDOWEVENT_RESTORED:
case SDL_WINDOWEVENT_FOCUS_GAINED:
// these mean the user reacted to the alert and
// it can now be stopped
sdl_forceUnmute = qfalse;
break;
default:
break;
}
}
static void sdl_X11( const XEvent* event )
{
switch (event->type) {
case FocusIn:
// see in_focusDelay explanation at the top
sdl_focusTime = Sys_Milliseconds();
sdl_focused = qtrue;
break;
case FocusOut:
// set modifier keys as released to prevent
// accidental combos such alt+enter right after
// getting focus
// e.g. alt gets "stuck", pressing only enter
// does a video restart as if pressing alt+enter
Lin_QueEvent(0, SE_KEY, K_ALT, qfalse, 0, NULL);
Lin_QueEvent(0, SE_KEY, K_CTRL, qfalse, 0, NULL);
Lin_QueEvent(0, SE_KEY, K_SHIFT, qfalse, 0, NULL);
sdl_focused = qfalse;
break;
default:
break;
}
}
static void sdl_Event( const SDL_Event* event )
{
// Note that CVar checks are necessary here because event polling
// can actually start before the main loop does,
// i.e. CVars can be uninitialized by the time we get here.
switch (event->type) {
case SDL_QUIT:
Com_Quit(0);
break;
case SDL_KEYDOWN:
// the CVar check means we'll ignore all keydown events until the main loop starts
if (in_focusDelay != NULL && sdl_focused && Sys_Milliseconds() - sdl_focusTime >= in_focusDelay->integer)
sdl_Key(&event->key, qtrue);
break;
case SDL_KEYUP:
// always forward releases
sdl_Key(&event->key, qfalse);
break;
case SDL_TEXTINPUT:
if (sdl_focused)
sdl_Text(&event->text);
break;
case SDL_MOUSEMOTION:
if (sdl_focused)
sdl_MouseMotion(&event->motion);
break;
case SDL_MOUSEBUTTONDOWN:
if (sdl_focused)
sdl_MouseButton(&event->button, qtrue);
break;
case SDL_MOUSEBUTTONUP:
// always forward releases
sdl_MouseButton(&event->button, qfalse);
break;
case SDL_MOUSEWHEEL:
if (sdl_focused)
sdl_MouseWheel(&event->wheel);
break;
case SDL_WINDOWEVENT:
sdl_Window(&event->window);
break;
case SDL_SYSWMEVENT:
{
const SDL_SysWMmsg* msg = event->syswm.msg;
if (msg->subsystem == SDL_SYSWM_X11)
sdl_X11(&msg->msg.x11.event);
}
break;
default:
break;
}
}
qbool sdl_Init()
{
atexit(SDL_Quit);
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) {
fprintf(stderr, "Failed to initialize SDL 2: %s\n", SDL_GetError());
return qfalse;
}
SDL_version version;
SDL_GetVersion(&version);
printf("Opened SDL %d.%d.%d\n", version.major, version.minor, version.patch);
// @TODO: investigate/test these?
// SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH
// SDL_HINT_MOUSE_RELATIVE_MODE_WARP
#if SDL_VERSION_ATLEAST(2, 0, 4)
if (sdl_Version_AtLeast(2, 0, 4))
SDL_SetHintWithPriority(SDL_HINT_NO_SIGNAL_HANDLERS, "1", SDL_HINT_OVERRIDE);
#endif
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_CRITICAL);
SDL_StartTextInput(); // enables SDL_TEXTINPUT events
SDL_EventState(SDL_SYSWMEVENT, SDL_ENABLE);
return qtrue;
}
void sdl_InitCvarsAndCmds()
{
Cvar_RegisterArray(in_cvars, MODULE_CLIENT);
Cmd_RegisterArray(in_cmds, MODULE_CLIENT);
}
void sdl_PollEvents()
{
SDL_Event event;
while (SDL_PollEvent(&event))
sdl_Event(&event);
}
static qbool sdl_IsInputActive()
{
if (in_noGrab->integer)
return qfalse;
const qbool isConsoleDown = (cls.keyCatchers & KEYCATCH_CONSOLE) != 0;
if (isConsoleDown && glimp.monitorCount >= 2)
return qfalse;
const qbool hasFocus = (SDL_GetWindowFlags(glimp.window) & SDL_WINDOW_INPUT_FOCUS) != 0;
if (!hasFocus)
return qfalse;
const qbool isFullScreen = Cvar_VariableIntegerValue("r_fullscreen");
if (isConsoleDown && !isFullScreen)
return qfalse;
return qtrue;
}
static void S_Frame()
{
if (sdl_forceUnmute) {
sdl_MuteAudio(qfalse);
return;
}
qbool mute = qfalse;
if (s_autoMute->integer == AMM_UNFOCUSED) {
const qbool hasFocus = (SDL_GetWindowFlags(glimp.window) & SDL_WINDOW_INPUT_FOCUS) != 0;
mute = !hasFocus;
} else if (s_autoMute->integer == AMM_MINIMIZED) {
const Uint32 hidingFlags = SDL_WINDOW_HIDDEN | SDL_WINDOW_MINIMIZED;
const qbool hidden = (SDL_GetWindowFlags(glimp.window) & hidingFlags) != 0;
mute = hidden;
}
sdl_MuteAudio(mute);
}
void sdl_Frame()
{
sdl_inputActive = sdl_IsInputActive();
sdl_PollEvents();
SDL_SetRelativeMouseMode((sdl_inputActive && m_relative->integer) ? SDL_TRUE : SDL_FALSE);
SDL_SetWindowGrab(glimp.window, sdl_inputActive ? SDL_TRUE : SDL_FALSE);
SDL_ShowCursor(sdl_inputActive ? SDL_DISABLE : SDL_ENABLE);
// @NOTE: SDL_WarpMouseInWindow generates a motion event
S_Frame();
}
void Sys_InitInput()
{
}
void Sys_ShutdownInput()
{
}
// returns the number of bytes to skip
static int UTF8_ReadNextChar( char* c, const char* input )
{
if (*input == '\0')
return 0;
const byte byte0 = (byte)input[0];
if (byte0 <= 127) {
*c = (char)byte0;
return 1;
}
// Starts with 110?
if ((byte0 >> 5) == 6)
return 2;
// Starts with 1110?
if ((byte0 >> 4) == 14)
return 3;
// Starts with 11110?
if ((byte0 >> 3) == 30)
return 4;
return 0;
}
char* Sys_GetClipboardData()
{
if (SDL_HasClipboardText() == SDL_FALSE)
return NULL;
char* const textUTF8 = SDL_GetClipboardText();
if (textUTF8 == NULL)
return NULL;
// the cleaned up string can only be
// as long or shorter
char* const text = (char*)Z_Malloc(strlen(textUTF8) + 1);
if (text == NULL) {
SDL_free(textUTF8);
return NULL;
}
// clean up the text so we're sure
// the console can display it
char* d = text;
const char* s = textUTF8;
for (;;) {
char c;
const int bytes = UTF8_ReadNextChar(&c, s);
if (bytes == 0) {
*d = '\0';
break;
}
if (c >= 0x20 && c <= 0x7E)
*d++ = c;
s += bytes;
}
SDL_free(textUTF8);
return text;
}
void Sys_SetClipboardData( const char* text )
{
SDL_SetClipboardText(text);
}
void Lin_MatchStartAlert()
{
const int alerts = cl_matchAlerts->integer;
const qbool unmuteBit = (alerts & MAF_UNMUTE) != 0;
if (!unmuteBit)
return;
const qbool unfocusedBit = (alerts & MAF_UNFOCUSED) != 0;
const qbool hasFocus = (SDL_GetWindowFlags(glimp.window) & SDL_WINDOW_INPUT_FOCUS) != 0;
const Uint32 hidingFlags = SDL_WINDOW_HIDDEN | SDL_WINDOW_MINIMIZED;
const qbool hidden = (SDL_GetWindowFlags(glimp.window) & hidingFlags) != 0;
if (hidden || (unfocusedBit && !hasFocus))
sdl_forceUnmute = qtrue;
}
void Lin_MatchEndAlert()
{
sdl_forceUnmute = qfalse;
}
void Sys_MatchAlert( sysMatchAlertEvent_t event )
{
if (event == SMAE_MATCH_START)
Lin_MatchStartAlert();
else if (event == SMAE_MATCH_END)
Lin_MatchEndAlert();
}
qbool Sys_IsMinimized()
{
return (glimp.window != NULL) && (SDL_GetWindowFlags(glimp.window) & SDL_WINDOW_MINIMIZED) != 0;
}