shithub: choc

Download patch

ref: bbc098c3ee39ed3167d020275bf1df039df8fd41
parent: 4025b46a5c2e96168e18673733216f984ba1deae
author: Simon Howard <[email protected]>
date: Thu May 15 21:12:36 EDT 2014

setup: Factor out axis configuration to widget.

Move code for configuring joystick axes into a separate widget, and
add axis widgets to the configuration window for all possible movement.

--- a/src/setup/Makefile.am
+++ b/src/setup/Makefile.am
@@ -18,6 +18,7 @@
     multiplayer.c     multiplayer.h             \
     sound.c           sound.h                   \
     execute.c         execute.h                 \
+    txt_joyaxis.c     txt_joyaxis.h             \
     txt_joybinput.c   txt_joybinput.h           \
     txt_keyinput.c    txt_keyinput.h            \
     txt_mouseinput.c  txt_mouseinput.h
--- a/src/setup/joystick.c
+++ b/src/setup/joystick.c
@@ -25,19 +25,9 @@
 #include "execute.h"
 #include "joystick.h"
 #include "mode.h"
+#include "txt_joyaxis.h"
 #include "txt_joybinput.h"
 
-typedef enum
-{
-    CALIBRATE_CENTER,
-    CALIBRATE_LEFT,
-    CALIBRATE_UP,
-
-    // These are only used when defining button axes:
-    CALIBRATE_RIGHT,
-    CALIBRATE_DOWN,
-} calibration_stage_t;
-
 typedef struct
 {
     char *name;  // Config file name
@@ -69,15 +59,6 @@
 
 static int calibrate_button = -1;
 
-// "Bad" joystick axes. Sometimes an axis can be stuck or "bad". An
-// example I found is that if you unplug the nunchuck extension from
-// a Wii remote, the axes from the nunchuck can be stuck at one of
-// the maximum values. These have to be ignored, so when we ask the
-// user to center the joystick, we look for bad axes that are not
-// close to zero.
-
-static boolean *bad_axis = NULL;
-
 // Which joystick axis to use for horizontal movement, and whether to
 // invert the direction:
 
@@ -101,6 +82,8 @@
 };
 
 static txt_button_t *joystick_button;
+static txt_joystick_axis_t *x_axis_widget;
+static txt_joystick_axis_t *y_axis_widget;
 
 //
 // Calibration
@@ -107,8 +90,6 @@
 //
 
 static txt_window_t *calibration_window;
-static txt_label_t *calibration_label;
-static calibration_stage_t calibrate_stage;
 static SDL_Joystick **all_joysticks = NULL;
 
 // Known controllers.
@@ -468,262 +449,13 @@
     all_joysticks = NULL;
 }
 
-static void SetCalibrationLabel(void)
+static void CalibrateXAxis(void)
 {
-    char *message = "???";
-
-    switch (calibrate_stage)
-    {
-        case CALIBRATE_CENTER:
-            message = "Move the D-pad or joystick to the\n"
-                      "center, and press a button.";
-            break;
-        case CALIBRATE_UP:
-            message = "Push the D-pad or joystick up,\n"
-                      "and press the button.";
-            break;
-        case CALIBRATE_LEFT:
-            message = "Push the D-pad or joystick to the\n"
-                      "left, and press the button.";
-            break;
-        case CALIBRATE_DOWN:
-            message = "Push the D-pad or joystick down,\n"
-                      "and press the button.";
-            break;
-        case CALIBRATE_RIGHT:
-            message = "Push the D-pad or joystick to the\n"
-                      "right, and press the button.";
-            break;
-    }
-
-    TXT_SetLabel(calibration_label, message);
+    TXT_ConfigureJoystickAxis(x_axis_widget, calibrate_button, NULL);
 }
 
-// Search all axes on joystick being configured; find a button that is
-// pressed (other than the calibrate button). Returns the button number.
-
-static int FindPressedAxisButton(void)
-{
-    SDL_Joystick *joystick;
-    int i;
-
-    joystick = all_joysticks[joystick_index];
-
-    for (i = 0; i < SDL_JoystickNumButtons(joystick); ++i)
-    {
-        if (i == calibrate_button)
-        {
-            continue;
-        }
-
-        if (SDL_JoystickGetButton(joystick, i))
-        {
-            return i;
-        }
-    }
-
-    return -1;
-}
-
-// Look for a hat that isn't centered. Returns the encoded hat axis.
-
-static int FindUncenteredHat(int *axis_invert)
-{
-    SDL_Joystick *joystick;
-    int i, hatval;
-
-    joystick = all_joysticks[joystick_index];
-
-    for (i = 0; i < SDL_JoystickNumHats(joystick); ++i)
-    {
-        hatval = SDL_JoystickGetHat(joystick, i);
-
-        switch (hatval)
-        {
-            case SDL_HAT_LEFT:
-            case SDL_HAT_RIGHT:
-                *axis_invert = hatval != SDL_HAT_LEFT;
-                return CREATE_HAT_AXIS(i, HAT_AXIS_HORIZONTAL);
-
-            case SDL_HAT_UP:
-            case SDL_HAT_DOWN:
-                *axis_invert = hatval != SDL_HAT_UP;
-                return CREATE_HAT_AXIS(i, HAT_AXIS_VERTICAL);
-
-            // If the hat is centered, or is not pointing in a
-            // definite direction, then ignore it. We don't accept
-            // the hat being pointed to the upper-left for example,
-            // because it's ambiguous.
-            case SDL_HAT_CENTERED:
-            default:
-                break;
-        }
-    }
-
-    // None found.
-    return -1;
-}
-
-static boolean CalibrateAxis(int *axis_index, int *axis_invert)
-{
-    SDL_Joystick *joystick;
-    int best_axis;
-    int best_value;
-    int best_invert;
-    Sint16 axis_value;
-    int i;
-
-    joystick = all_joysticks[joystick_index];
-
-    // Check all axes to find which axis has the largest value.  We test
-    // for one axis at a time, so eg. when we prompt to push the joystick 
-    // left, whichever axis has the largest value is the left axis.
-
-    best_axis = 0;
-    best_value = 0;
-    best_invert = 0;
-
-    for (i=0; i<SDL_JoystickNumAxes(joystick); ++i)
-    {
-        if (bad_axis[i])
-        {
-            continue;
-        }
-
-        axis_value = SDL_JoystickGetAxis(joystick, i);
-
-        if (abs(axis_value) > best_value)
-        {
-            best_value = abs(axis_value);
-            best_invert = axis_value > 0;
-            best_axis = i;
-        }
-    }
-
-    // Did we find one axis that had a significant value?
-
-    if (best_value > 32768 / 4)
-    {
-        // Save the best values we have found
-
-        *axis_index = best_axis;
-        *axis_invert = best_invert;
-        return true;
-    }
-
-    // Otherwise, maybe this is a "button axis", like the PS3 SIXAXIS
-    // controller that exposes the D-pad as four individual buttons.
-    // Search for a button.
-
-    i = FindPressedAxisButton();
-
-    if (i >= 0)
-    {
-        *axis_index = CREATE_BUTTON_AXIS(i, 0);
-        *axis_invert = 0;
-        return true;
-    }
-
-    // Maybe it's a D-pad that is presented as a hat. This sounds weird
-    // but gamepads like this really do exist; an example is the
-    // Nyko AIRFLO Ex.
-
-    i = FindUncenteredHat(axis_invert);
-
-    if (i >= 0)
-    {
-        *axis_index = i;
-        return true;
-    }
-
-    // User pressed the button without pushing the joystick anywhere.
-    return false;
-}
-
-static boolean SetButtonAxisPositive(int *axis_index)
-{
-    int button;
-
-    button = FindPressedAxisButton();
-
-    if (button >= 0)
-    {
-        *axis_index |= CREATE_BUTTON_AXIS(0, button);
-        return true;
-    }
-
-    return false;
-}
-
-static int NextCalibrateStage(void)
-{
-    switch (calibrate_stage)
-    {
-        case CALIBRATE_CENTER:
-            return CALIBRATE_LEFT;
-
-        // After pushing to the left, there are two possibilities:
-        // either it is a button axis, in which case we need to find
-        // the other button, or we can just move on to the next axis.
-        case CALIBRATE_LEFT:
-            if (IS_BUTTON_AXIS(joystick_x_axis))
-            {
-                return CALIBRATE_RIGHT;
-            }
-            else
-            {
-                return CALIBRATE_UP;
-            }
-
-        case CALIBRATE_RIGHT:
-            return CALIBRATE_UP;
-
-        case CALIBRATE_UP:
-            if (IS_BUTTON_AXIS(joystick_y_axis))
-            {
-                return CALIBRATE_DOWN;
-            }
-            else
-            {
-                // Finished.
-                return CALIBRATE_CENTER;
-            }
-
-        case CALIBRATE_DOWN:
-            // Finished.
-            return CALIBRATE_CENTER;
-    }
-}
-
-static void IdentifyBadAxes(void)
-{
-    SDL_Joystick *joystick;
-    int i, val;
-
-    free(bad_axis);
-
-    joystick = all_joysticks[joystick_index];
-    bad_axis = calloc(SDL_JoystickNumAxes(joystick), sizeof(boolean));
-
-    // Look for uncentered axes.
-
-    for (i = 0; i < SDL_JoystickNumAxes(joystick); ++i)
-    {
-        val = SDL_JoystickGetAxis(joystick, i);
-
-        bad_axis[i] = abs(val) > (32768 / 5);
-
-        if (bad_axis[i])
-        {
-            printf("Ignoring uncentered joystick axis #%i: %i\n", i, val);
-        }
-    }
-}
-
 static int CalibrationEventCallback(SDL_Event *event, void *user_data)
 {
-    boolean advance;
-
     if (event->type != SDL_JOYBUTTONDOWN)
     {
         return 0;
@@ -732,75 +464,28 @@
     // At this point, we have a button press.
     // In the first "center" stage, we're just trying to work out which
     // joystick is being configured and which button the user is pressing.
-    if (calibrate_stage == CALIBRATE_CENTER)
-    {
-        joystick_index = event->jbutton.which;
-        calibrate_button = event->jbutton.button;
-        IdentifyBadAxes();
+    joystick_index = event->jbutton.which;
+    calibrate_button = event->jbutton.button;
 
-        // If the joystick is a known one, auto-load default
-        // config for it.
-        if (IsKnownJoystick(joystick_index))
-        {
-            LoadKnownConfiguration();
-            usejoystick = 1;
-            TXT_CloseWindow(calibration_window);
-        }
-        else
-        {
-            // Advance to next stage.
-            calibrate_stage = CALIBRATE_LEFT;
-            SetCalibrationLabel();
-        }
-
-        return 1;
+    // If the joystick is a known one, auto-load default
+    // config for it. Otherwise, proceed with calibration.
+    if (IsKnownJoystick(joystick_index))
+    {
+        LoadKnownConfiguration();
+        usejoystick = 1;
+        TXT_CloseWindow(calibration_window);
     }
-
-    // In subsequent stages, the user is asked to push in a specific
-    // direction and press the button. They must push the same button
-    // as they did before; this is necessary to support button axes.
-    if (event->jbutton.which == joystick_index
-     && event->jbutton.button == calibrate_button)
+    else
     {
-        switch (calibrate_stage)
-        {
-            default:
-            case CALIBRATE_LEFT:
-                advance = CalibrateAxis(&joystick_x_axis, &joystick_x_invert);
-                break;
+        TXT_CloseWindow(calibration_window);
 
-            case CALIBRATE_RIGHT:
-                advance = SetButtonAxisPositive(&joystick_x_axis);
-                break;
-
-            case CALIBRATE_UP:
-                advance = CalibrateAxis(&joystick_y_axis, &joystick_y_invert);
-                break;
-
-            case CALIBRATE_DOWN:
-                advance = SetButtonAxisPositive(&joystick_y_axis);
-                break;
-        }
-
-        // Advance to the next calibration stage?
-
-        if (advance)
-        {
-            calibrate_stage = NextCalibrateStage();
-            SetCalibrationLabel();
-
-            // Finished?
-            if (calibrate_stage == CALIBRATE_CENTER)
-            {
-                usejoystick = 1;
-                TXT_CloseWindow(calibration_window);
-            }
-
-            return 1;
-        }
+        // Calibrate joystick axes: Y axis first, then X axis once
+        // completed.
+        TXT_ConfigureJoystickAxis(y_axis_widget, calibrate_button,
+                                  CalibrateXAxis);
     }
 
-    return 0;
+    return 1;
 }
 
 static void NoJoystick(void)
@@ -823,8 +508,6 @@
 
 static void CalibrateJoystick(TXT_UNCAST_ARG(widget), TXT_UNCAST_ARG(unused))
 {
-    calibrate_stage = CALIBRATE_CENTER;
-
     // Try to open all available joysticks.  If none are opened successfully,
     // bomb out with an error.
 
@@ -837,10 +520,9 @@
     calibration_window = TXT_NewWindow("Gamepad/Joystick calibration");
 
     TXT_AddWidgets(calibration_window,
-                   TXT_NewLabel("Please follow the following instructions\n"
-                                "in order to calibrate your controller."),
                    TXT_NewStrut(0, 1),
-                   calibration_label = TXT_NewLabel("zzz"),
+                   TXT_NewLabel("Center the D-pad or joystick,\n"
+                                "and press a button."),
                    TXT_NewStrut(0, 1),
                    NULL);
 
@@ -849,7 +531,6 @@
                         TXT_NewWindowAbortAction(calibration_window));
     TXT_SetWindowAction(calibration_window, TXT_HORIZ_RIGHT, NULL);
 
-    TXT_SetWidgetAlign(calibration_label, TXT_HORIZ_CENTER);
     TXT_SDL_SetEventCallback(CalibrationEventCallback, NULL);
 
     TXT_SignalConnect(calibration_window, "closed", CalibrateWindowClosed, NULL);
@@ -857,9 +538,6 @@
     // Start calibration
 
     joystick_index = -1;
-    calibrate_stage = CALIBRATE_CENTER;
-
-    SetCalibrationLabel();
 }
 
 //
@@ -888,10 +566,10 @@
 void ConfigJoystick(void)
 {
     txt_window_t *window;
-    txt_table_t *button_table;
+    txt_table_t *button_table, *axis_table;
     txt_table_t *joystick_table;
 
-    if (!joystick_initted) 
+    if (!joystick_initted)
     {
         joystick_initted = SDL_Init(SDL_INIT_JOYSTICK) >= 0;
     }
@@ -901,10 +579,29 @@
     TXT_AddWidgets(window,
                    TXT_NewCheckBox("Enable gamepad/joystick", &usejoystick),
                    joystick_table = TXT_NewTable(2),
+                   TXT_NewSeparator("Axes"),
+                   axis_table = TXT_NewTable(2),
                    TXT_NewSeparator("Buttons"),
-                   button_table = TXT_NewTable(2),
+                   button_table = TXT_NewTable(4),
                    NULL);
 
+    TXT_SetColumnWidths(axis_table, 20, 15);
+
+    TXT_AddWidgets(axis_table,
+                   TXT_NewLabel("Forward/backward"),
+                   y_axis_widget = TXT_NewJoystickAxis(&joystick_y_axis,
+                                                       &joystick_y_invert,
+                                                       JOYSTICK_AXIS_VERTICAL),
+                   TXT_NewLabel("Turn left/right"),
+                   x_axis_widget = TXT_NewJoystickAxis(&joystick_x_axis,
+                                                       &joystick_x_invert,
+                                                       JOYSTICK_AXIS_HORIZONTAL),
+                   TXT_NewLabel("Strafe left/right"),
+                   TXT_NewJoystickAxis(&joystick_strafe_axis,
+                                       &joystick_strafe_invert,
+                                        JOYSTICK_AXIS_HORIZONTAL),
+                   NULL);
+
     TXT_SetColumnWidths(joystick_table, 20, 15);
 
     TXT_AddWidgets(joystick_table,
@@ -912,11 +609,19 @@
                    joystick_button = TXT_NewButton("zzzz"),
                    NULL);
 
-    TXT_SetColumnWidths(button_table, 20, 15);
+    TXT_SetColumnWidths(button_table, 16, 12, 14, 11);
 
     AddJoystickControl(button_table, "Fire/Attack", &joybfire);
+    AddJoystickControl(button_table, "Strafe Left", &joybstrafeleft);
+
     AddJoystickControl(button_table, "Use", &joybuse);
+    AddJoystickControl(button_table, "Strafe Right", &joybstraferight);
 
+    AddJoystickControl(button_table, "Previous weapon", &joybprevweapon);
+    AddJoystickControl(button_table, "Strafe", &joybstrafe);
+
+    AddJoystickControl(button_table, "Next weapon", &joybnextweapon);
+
     // High values of joybspeed are used to activate the "always run mode"
     // trick in Vanilla Doom.  If this has been enabled, not only is the
     // joybspeed value meaningless, but the control itself is useless.
@@ -925,13 +630,6 @@
     {
         AddJoystickControl(button_table, "Speed", &joybspeed);
     }
-
-    AddJoystickControl(button_table, "Strafe", &joybstrafe);
-
-    AddJoystickControl(button_table, "Strafe Left", &joybstrafeleft);
-    AddJoystickControl(button_table, "Strafe Right", &joybstraferight);
-    AddJoystickControl(button_table, "Previous weapon", &joybprevweapon);
-    AddJoystickControl(button_table, "Next weapon", &joybnextweapon);
 
     if (gamemission == hexen || gamemission == strife)
     {
--- /dev/null
+++ b/src/setup/txt_joyaxis.c
@@ -1,0 +1,527 @@
+//
+// Copyright(C) 2014 Simon Howard
+//
+// 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 2
+// 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "SDL.h"
+
+#include "joystick.h"
+#include "i_joystick.h"
+#include "i_system.h"
+#include "m_controls.h"
+#include "m_misc.h"
+
+#include "textscreen.h"
+#include "txt_gui.h"
+#include "txt_io.h"
+#include "txt_joyaxis.h"
+
+#define JOYSTICK_AXIS_WIDTH 24
+
+static char *CalibrationLabel(txt_joystick_axis_t *joystick_axis)
+{
+    switch (joystick_axis->config_stage)
+    {
+        case CONFIG_CENTER:
+            return "Center the D-pad or joystick,\n"
+                   "and press a button.";
+
+        case CONFIG_STAGE1:
+            if (joystick_axis->dir == JOYSTICK_AXIS_VERTICAL)
+            {
+                return "Push the D-pad or joystick up,\n"
+                       "and press the button.";
+            }
+            else
+            {
+                return "Push the D-pad or joystick to the\n"
+                       "left, and press the button.";
+            }
+
+        case CONFIG_STAGE2:
+            if (joystick_axis->dir == JOYSTICK_AXIS_VERTICAL)
+            {
+                return "Push the D-pad or joystick down,\n"
+                       "and press the button.";
+            }
+            else
+            {
+                return "Push the D-pad or joystick to the\n"
+                       "right, and press the button.";
+            }
+    }
+}
+
+static void SetCalibrationLabel(txt_joystick_axis_t *joystick_axis)
+{
+    TXT_SetLabel(joystick_axis->config_label, CalibrationLabel(joystick_axis));
+}
+
+// Search all axes on joystick being configured; find a button that is
+// pressed (other than the calibrate button). Returns the button number.
+
+static int FindPressedAxisButton(txt_joystick_axis_t *joystick_axis)
+{
+    int i;
+
+    for (i = 0; i < SDL_JoystickNumButtons(joystick_axis->joystick); ++i)
+    {
+        if (i == joystick_axis->config_button)
+        {
+            continue;
+        }
+
+        if (SDL_JoystickGetButton(joystick_axis->joystick, i))
+        {
+            return i;
+        }
+    }
+
+    return -1;
+}
+
+// Look for a hat that isn't centered. Returns the encoded hat axis.
+
+static int FindUncenteredHat(SDL_Joystick *joystick, int *axis_invert)
+{
+    int i, hatval;
+
+    for (i = 0; i < SDL_JoystickNumHats(joystick); ++i)
+    {
+        hatval = SDL_JoystickGetHat(joystick, i);
+
+        switch (hatval)
+        {
+            case SDL_HAT_LEFT:
+            case SDL_HAT_RIGHT:
+                *axis_invert = hatval != SDL_HAT_LEFT;
+                return CREATE_HAT_AXIS(i, HAT_AXIS_HORIZONTAL);
+
+            case SDL_HAT_UP:
+            case SDL_HAT_DOWN:
+                *axis_invert = hatval != SDL_HAT_UP;
+                return CREATE_HAT_AXIS(i, HAT_AXIS_VERTICAL);
+
+            // If the hat is centered, or is not pointing in a
+            // definite direction, then ignore it. We don't accept
+            // the hat being pointed to the upper-left for example,
+            // because it's ambiguous.
+            case SDL_HAT_CENTERED:
+            default:
+                break;
+        }
+    }
+
+    // None found.
+    return -1;
+}
+
+static boolean CalibrateAxis(txt_joystick_axis_t *joystick_axis)
+{
+    int best_axis;
+    int best_value;
+    int best_invert;
+    Sint16 axis_value;
+    int i;
+
+    // Check all axes to find which axis has the largest value.  We test
+    // for one axis at a time, so eg. when we prompt to push the joystick 
+    // left, whichever axis has the largest value is the left axis.
+
+    best_axis = 0;
+    best_value = 0;
+    best_invert = 0;
+
+    for (i = 0; i < SDL_JoystickNumAxes(joystick_axis->joystick); ++i)
+    {
+       //if (bad_axis[i])
+       //{
+       //    continue;
+       //}
+
+        axis_value = SDL_JoystickGetAxis(joystick_axis->joystick, i);
+
+        if (abs(axis_value) > best_value)
+        {
+            best_value = abs(axis_value);
+            best_invert = axis_value > 0;
+            best_axis = i;
+        }
+    }
+
+    // Did we find one axis that had a significant value?
+
+    if (best_value > 32768 / 4)
+    {
+        // Save the best values we have found
+
+        *joystick_axis->axis = best_axis;
+        *joystick_axis->invert = best_invert;
+        return true;
+    }
+
+    // Otherwise, maybe this is a "button axis", like the PS3 SIXAXIS
+    // controller that exposes the D-pad as four individual buttons.
+    // Search for a button.
+
+    i = FindPressedAxisButton(joystick_axis);
+
+    if (i >= 0)
+    {
+        *joystick_axis->axis = CREATE_BUTTON_AXIS(i, 0);
+        *joystick_axis->invert = 0;
+        return true;
+    }
+
+    // Maybe it's a D-pad that is presented as a hat. This sounds weird
+    // but gamepads like this really do exist; an example is the
+    // Nyko AIRFLO Ex.
+
+    i = FindUncenteredHat(joystick_axis->joystick, joystick_axis->invert);
+
+    if (i >= 0)
+    {
+        *joystick_axis->axis = i;
+        return true;
+    }
+
+    // User pressed the button without pushing the joystick anywhere.
+    return false;
+}
+
+static boolean SetButtonAxisPositive(txt_joystick_axis_t *joystick_axis)
+{
+    int button;
+
+    button = FindPressedAxisButton(joystick_axis);
+
+    if (button >= 0)
+    {
+        *joystick_axis->axis |= CREATE_BUTTON_AXIS(0, button);
+        return true;
+    }
+
+    return false;
+}
+
+static void IdentifyBadAxes(txt_joystick_axis_t *joystick_axis)
+{
+    int i, val;
+
+    free(joystick_axis->bad_axis);
+
+    joystick_axis->bad_axis
+        = calloc(SDL_JoystickNumAxes(joystick_axis->joystick),
+                                     sizeof(boolean));
+
+    // Look for uncentered axes.
+
+    for (i = 0; i < SDL_JoystickNumAxes(joystick_axis->joystick); ++i)
+    {
+        val = SDL_JoystickGetAxis(joystick_axis->joystick, i);
+
+        joystick_axis->bad_axis[i] = abs(val) > (32768 / 5);
+
+        if (joystick_axis->bad_axis[i])
+        {
+            printf("Ignoring uncentered joystick axis #%i: %i\n", i, val);
+        }
+    }
+}
+
+static int NextCalibrateStage(txt_joystick_axis_t *joystick_axis)
+{
+    switch (joystick_axis->config_stage)
+    {
+        case CONFIG_CENTER:
+            return CONFIG_STAGE1;
+
+        // After pushing to the left, there are two possibilities:
+        // either it is a button axis, in which case we need to find
+        // the other button, or we can just move on to the next axis.
+        case CONFIG_STAGE1:
+            if (IS_BUTTON_AXIS(*joystick_axis->axis))
+            {
+                return CONFIG_STAGE2;
+            }
+            else
+            {
+                return CONFIG_CENTER;
+            }
+
+        case CONFIG_STAGE2:
+            return CONFIG_CENTER;
+    }
+}
+
+static int EventCallback(SDL_Event *event, TXT_UNCAST_ARG(joystick_axis))
+{
+    TXT_CAST_ARG(txt_joystick_axis_t, joystick_axis);
+    boolean advance;
+
+    if (event->type != SDL_JOYBUTTONDOWN)
+    {
+        return 0;
+    }
+
+    // At this point, we have a button press.
+    // In the first "center" stage, we're just trying to work out which
+    // joystick is being configured and which button the user is pressing.
+    if (joystick_axis->config_stage == CONFIG_CENTER)
+    {
+        joystick_index = event->jbutton.which;
+        joystick_axis->config_button = event->jbutton.button;
+        IdentifyBadAxes(joystick_axis);
+
+        // Advance to next stage.
+        joystick_axis->config_stage = CONFIG_STAGE1;
+        SetCalibrationLabel(joystick_axis);
+
+        return 1;
+    }
+
+    // In subsequent stages, the user is asked to push in a specific
+    // direction and press the button. They must push the same button
+    // as they did before; this is necessary to support button axes.
+    if (event->jbutton.which == joystick_index
+     && event->jbutton.button == joystick_axis->config_button)
+    {
+        switch (joystick_axis->config_stage)
+        {
+            default:
+            case CONFIG_STAGE1:
+                advance = CalibrateAxis(joystick_axis);
+                break;
+
+            case CONFIG_STAGE2:
+                advance = SetButtonAxisPositive(joystick_axis);
+                break;
+        }
+
+        // Advance to the next calibration stage?
+
+        if (advance)
+        {
+            joystick_axis->config_stage = NextCalibrateStage(joystick_axis);
+            SetCalibrationLabel(joystick_axis);
+
+            // Finished?
+            if (joystick_axis->config_stage == CONFIG_CENTER)
+            {
+                TXT_CloseWindow(joystick_axis->config_window);
+
+                if (joystick_axis->callback != NULL)
+                {
+                    joystick_axis->callback();
+                }
+            }
+
+            return 1;
+        }
+    }
+
+    return 0;
+}
+
+static void CalibrateWindowClosed(TXT_UNCAST_ARG(widget),
+                                  TXT_UNCAST_ARG(joystick_axis))
+{
+    TXT_CAST_ARG(txt_joystick_axis_t, joystick_axis);
+
+    free(joystick_axis->bad_axis);
+    joystick_axis->bad_axis = NULL;
+
+    SDL_JoystickClose(joystick_axis->joystick);
+    SDL_JoystickEventState(SDL_DISABLE);
+    SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
+    TXT_SDL_SetEventCallback(NULL, NULL);
+}
+
+void TXT_ConfigureJoystickAxis(txt_joystick_axis_t *joystick_axis,
+                               int using_button,
+                               txt_joystick_axis_callback_t callback)
+{
+    // Open the joystick first.
+    if (SDL_Init(SDL_INIT_JOYSTICK) < 0)
+    {
+        return;
+    }
+
+    joystick_axis->joystick = SDL_JoystickOpen(joystick_index);
+    if (joystick_axis->joystick == NULL)
+    {
+        // TODO: OpenErrorWindow();
+        return;
+    }
+
+    SDL_JoystickEventState(SDL_ENABLE);
+
+    // Build the prompt window.
+
+    joystick_axis->config_window
+        = TXT_NewWindow("Gamepad/Joystick calibration");
+    TXT_AddWidgets(joystick_axis->config_window,
+                   TXT_NewStrut(0, 1),
+                   joystick_axis->config_label = TXT_NewLabel(""),
+                   TXT_NewStrut(0, 1),
+                   NULL);
+
+    TXT_SetWindowAction(joystick_axis->config_window, TXT_HORIZ_LEFT, NULL);
+    TXT_SetWindowAction(joystick_axis->config_window, TXT_HORIZ_CENTER,
+                        TXT_NewWindowAbortAction(joystick_axis->config_window));
+    TXT_SetWindowAction(joystick_axis->config_window, TXT_HORIZ_RIGHT, NULL);
+    TXT_SetWidgetAlign(joystick_axis->config_window, TXT_HORIZ_CENTER);
+
+    if (using_button >= 0)
+    {
+        joystick_axis->config_stage = CONFIG_STAGE1;
+        joystick_axis->config_button = using_button;
+        IdentifyBadAxes(joystick_axis);
+    }
+    else
+    {
+        joystick_axis->config_stage = CONFIG_CENTER;
+    }
+
+    SetCalibrationLabel(joystick_axis);
+
+    // Close the joystick and shut down joystick subsystem when the window
+    // is closed.
+    TXT_SignalConnect(joystick_axis->config_window, "closed",
+                      CalibrateWindowClosed, joystick_axis);
+
+    TXT_SDL_SetEventCallback(EventCallback, joystick_axis);
+
+    // When successfully calibrated, invoke this callback:
+    joystick_axis->callback = callback;
+}
+
+static void TXT_JoystickAxisSizeCalc(TXT_UNCAST_ARG(joystick_axis))
+{
+    TXT_CAST_ARG(txt_joystick_axis_t, joystick_axis);
+
+    // All joystickinputs are the same size.
+
+    joystick_axis->widget.w = JOYSTICK_AXIS_WIDTH;
+    joystick_axis->widget.h = 1;
+}
+
+static void TXT_JoystickAxisDrawer(TXT_UNCAST_ARG(joystick_axis))
+{
+    TXT_CAST_ARG(txt_joystick_axis_t, joystick_axis);
+    char buf[JOYSTICK_AXIS_WIDTH + 1];
+    int i;
+
+    if (*joystick_axis->axis < 0)
+    {
+        M_StringCopy(buf, "(None)", sizeof(buf));
+    }
+    else if (IS_BUTTON_AXIS(*joystick_axis->axis))
+    {
+        int neg, pos;
+
+        neg = BUTTON_AXIS_NEG(*joystick_axis->axis);
+        pos = BUTTON_AXIS_POS(*joystick_axis->axis);
+        M_snprintf(buf, sizeof(buf), "BUTTONS #%i+#%i", neg, pos);
+    }
+    else if (IS_HAT_AXIS(*joystick_axis->axis))
+    {
+        int hat, dir;
+
+        hat = HAT_AXIS_HAT(*joystick_axis->axis);
+        dir = HAT_AXIS_DIRECTION(*joystick_axis->axis);
+
+        M_snprintf(buf, sizeof(buf), "HAT #%i (%s)", hat,
+                   dir == HAT_AXIS_HORIZONTAL ? "horizontal" : "vertical");
+    }
+    else
+    {
+        M_snprintf(buf, sizeof(buf), "AXIS #%i", *joystick_axis->axis);
+    }
+
+    TXT_SetWidgetBG(joystick_axis);
+    TXT_FGColor(TXT_COLOR_BRIGHT_WHITE);
+
+    TXT_DrawString(buf);
+
+    for (i=strlen(buf); i<JOYSTICK_AXIS_WIDTH; ++i)
+    {
+        TXT_DrawString(" ");
+    }
+}
+
+static void TXT_JoystickAxisDestructor(TXT_UNCAST_ARG(joystick_axis))
+{
+}
+
+static int TXT_JoystickAxisKeyPress(TXT_UNCAST_ARG(joystick_axis), int key)
+{
+    TXT_CAST_ARG(txt_joystick_axis_t, joystick_axis);
+
+    if (key == KEY_ENTER)
+    {
+        TXT_ConfigureJoystickAxis(joystick_axis, -1, NULL);
+        return 1;
+    }
+
+    if (key == KEY_BACKSPACE || key == KEY_DEL)
+    {
+        *joystick_axis->axis = -1;
+    }
+
+    return 0;
+}
+
+static void TXT_JoystickAxisMousePress(TXT_UNCAST_ARG(widget),
+                                       int x, int y, int b)
+{
+    TXT_CAST_ARG(txt_joystick_axis_t, widget);
+
+    // Clicking is like pressing enter
+
+    if (b == TXT_MOUSE_LEFT)
+    {
+        TXT_JoystickAxisKeyPress(widget, KEY_ENTER);
+    }
+}
+
+txt_widget_class_t txt_joystick_axis_class =
+{
+    TXT_AlwaysSelectable,
+    TXT_JoystickAxisSizeCalc,
+    TXT_JoystickAxisDrawer,
+    TXT_JoystickAxisKeyPress,
+    TXT_JoystickAxisDestructor,
+    TXT_JoystickAxisMousePress,
+    NULL,
+};
+
+txt_joystick_axis_t *TXT_NewJoystickAxis(int *axis, int *invert,
+                                         txt_joystick_axis_direction_t dir)
+{
+    txt_joystick_axis_t *joystick_axis;
+
+    joystick_axis = malloc(sizeof(txt_joystick_axis_t));
+
+    TXT_InitWidget(joystick_axis, &txt_joystick_axis_class);
+    joystick_axis->axis = axis;
+    joystick_axis->invert = invert;
+    joystick_axis->dir = dir;
+    joystick_axis->bad_axis = NULL;
+
+    return joystick_axis;
+}
+
--- /dev/null
+++ b/src/setup/txt_joyaxis.h
@@ -1,0 +1,90 @@
+//
+// Copyright(C) 2005-2014 Simon Howard
+//
+// 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 2
+// 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.
+//
+
+#ifndef TXT_JOY_AXIS_H
+#define TXT_JOY_AXIS_H
+
+typedef struct txt_joystick_axis_s txt_joystick_axis_t;
+
+typedef enum
+{
+    JOYSTICK_AXIS_HORIZONTAL,
+    JOYSTICK_AXIS_VERTICAL,
+} txt_joystick_axis_direction_t;
+
+typedef enum
+{
+    CONFIG_CENTER,      // "Center the joystick and press a button..."
+    CONFIG_STAGE1,      // "Top or left and press a button..."
+    CONFIG_STAGE2,      // [Optional] "Bottom or right and press a button..."
+} txt_joystick_axis_stage_t;
+
+// Callback invoked when calibration is completed.
+typedef void (*txt_joystick_axis_callback_t)(void);
+
+#include "txt_widget.h"
+#include "txt_window.h"
+
+#include "SDL.h"
+
+//
+// A joystick axis.
+//
+
+struct txt_joystick_axis_s
+{
+    txt_widget_t widget;
+    int *axis, *invert;
+    txt_joystick_axis_direction_t dir;
+
+    // Only used when configuring:
+
+    // Configuration prompt window and label.
+    txt_window_t *config_window;
+    txt_label_t *config_label;
+
+    // SDL joystick handle for reading joystick state.
+    SDL_Joystick *joystick;
+
+    // "Bad" joystick axes. Sometimes an axis can be stuck or "bad". An
+    // example I found is that if you unplug the nunchuck extension from
+    // a Wii remote, the axes from the nunchuck can be stuck at one of
+    // the maximum values. These have to be ignored, so when we ask the
+    // user to center the joystick, we look for bad axes that are not
+    // close to zero.
+    boolean *bad_axis;
+
+    // Stage we have reached in configuring joystick axis.
+    txt_joystick_axis_stage_t config_stage;
+
+    // Button to press to advance to next stage.
+    int config_button;
+
+    // Callback invoked when the axis is calibrated.
+    txt_joystick_axis_callback_t callback;
+};
+
+txt_joystick_axis_t *TXT_NewJoystickAxis(int *axis, int *invert,
+                                         txt_joystick_axis_direction_t dir);
+
+// Configure a joystick axis widget.
+//   axis: The axis widget to configure.
+//   using_button: If non-negative, use this joystick button as the button
+//       to expect from the user. Otherwise, ask.
+void TXT_ConfigureJoystickAxis(txt_joystick_axis_t *axis, int using_button,
+                               txt_joystick_axis_callback_t callback);
+
+#endif /* #ifndef TXT_JOY_AXIS_H */
+
+