shithub: choc

Download patch

ref: d1a3967194323b08227b20822acedb837e05281a
parent: 6a2d4763a9080cf88ca9f0b588b8187963eeacf5
parent: e225e0c93ce58bb0e33c174847305d39800fd755
author: Simon Howard <fraggle@gmail.com>
date: Fri Dec 10 18:46:22 EST 2010

Merge from trunk.

Subversion-branch: /branches/raven-branch
Subversion-revision: 2214

--- a/NEWS
+++ b/NEWS
@@ -22,18 +22,20 @@
        devices (ie. without a proper keyboard) much more practical.
      * There is now a key binding to change the multiplayer spy key
        (usually F12).
-     * There is now a configuration file parameter to set the OPL I/O
-       port, for cards that don't use port 0x388.
+     * The setup tool now has a "warp" button on the main menu, like
+       Vanilla setup.exe (thanks Proteh).
      * Up to 8 mouse buttons are now supported (including the
        mousewheel).
+     * A new command line parameter has been added (-solo-net) which
+       can be used to simulate being in a single player netgame.
+     * There is now a configuration file parameter to set the OPL I/O
+       port, for cards that don't use port 0x388.
      * The Python scripts used for building Chocolate Doom now work
        with Python 3 (but also continue to work with Python 2)
        (thanks arin).
-     * The font used for the textscreen library can be forced by
-       setting the TEXTSCREEN_FONT environment variable to "small" or
-       "normal".
      * There is now a NOT-BUGS file included that lists some common
-       Vanilla Doom bugs/limitations that you might encounter.
+       Vanilla Doom bugs/limitations that you might encounter
+       (thanks to Sander van Dijk for feedback).
 
     Compatibility:
      * The -timer and -avg options now work the same as Vanilla when
@@ -43,6 +45,11 @@
      * The HacX v1.2 IWAD file is now supported, and can be used
        standalone without the need for the Doom II IWAD (thanks
        atyth).
+     * The I_Error function doesn't display "Error:" before the error
+       message, matching the Vanilla behavior.  "Error" has also been
+       removed from the title of the dialog box that appears on
+       Windows when this happens.  This is desirable as not all such
+       messages are actually errors (thanks Proteh).
 
     Bugs fixed:
      * A workaround has been a bug in old versions of SDL_mixer
@@ -70,6 +77,17 @@
      * Screen borders no longer flash when running on widescreen
        monitors, if you choose a true-color screen mode (thanks
        exp(x)).
+     * The controller player in a netgame is the first player to join,
+       instead of just being someone who gets lucky.
+
+    libtextscreen:
+     * The font used for the textscreen library can be forced by
+       setting the TEXTSCREEN_FONT environment variable to "small" or
+       "normal".
+     * Tables or scroll panes that don't contain any selectable widgets
+       are now themselves not selectable (thanks Proteh).
+     * The actions displayed at the bottom of windows are now laid out
+       in a more aesthetically pleasing way.
 
 1.4.0 (2010-07-10):
 
--- a/NOT-BUGS
+++ b/NOT-BUGS
@@ -65,7 +65,7 @@
 when you save the game, the game will quit with the message "Savegame
 buffer overrun".
 
-Vanilla Doom has a limited size memory bufferthat it uses for saving
+Vanilla Doom has a limited size memory buffer that it uses for saving
 games.  If you are playing on a large level, the buffer may be too
 small for the entire savegame to fit.  Chocolate Doom allows the limit
 to be disabled: in the setup tool, go to the "compatibility" menu and
--- a/codeblocks/server.cbp
+++ b/codeblocks/server.cbp
@@ -84,6 +84,10 @@
 			<Option compilerVar="CC" />
 		</Unit>
 		<Unit filename="..\src\net_packet.h" />
+		<Unit filename="..\src\net_query.c">
+			<Option compilerVar="CC" />
+		</Unit>
+		<Unit filename="..\src\net_query.h" />
 		<Unit filename="..\src\net_sdl.c">
 			<Option compilerVar="CC" />
 		</Unit>
--- a/src/doom/d_net.c
+++ b/src/doom/d_net.c
@@ -204,8 +204,8 @@
 	G_BuildTiccmd(&cmd);
 
 #ifdef FEATURE_MULTIPLAYER
-        
-        if (netgame && !demoplayback)
+
+        if (net_client_connected)
         {
             NET_CL_SendTiccmd(&cmd, maketic);
         }
@@ -460,6 +460,19 @@
     recvtic = 0;
 
     playeringame[0] = true;
+
+    //!
+    // @category net
+    //
+    // Start the game playing as though in a netgame with a single
+    // player.  This can also be used to play back single player netgame
+    // demos.
+    //
+
+    if (M_CheckParm("-solo-net") > 0)
+    {
+        netgame = true;
+    }
 }
 
 boolean D_InitNetGame(net_connect_data_t *connect_data,
@@ -467,6 +480,7 @@
 {
     net_addr_t *addr = NULL;
     int i;
+
 
 #ifdef FEATURE_MULTIPLAYER
 
--- a/src/doom/g_game.c
+++ b/src/doom/g_game.c
@@ -2145,16 +2145,11 @@
     for (i=0 ; i<MAXPLAYERS ; i++) 
 	playeringame[i] = *demo_p++; 
 
-    //!
-    // @category demo
-    // 
-    // Play back a demo recorded in a netgame with a single player.
-    //
-
-    if (playeringame[1] || M_CheckParm("-netdemo") > 0) 
-    { 
-	netgame = true; 
-	netdemo = true; 
+    if (playeringame[1] || M_CheckParm("-solo-net") > 0
+                        || M_CheckParm("-netdemo") > 0)
+    {
+	netgame = true;
+	netdemo = true;
     }
 
     // don't spend a lot of time in loadlevel 
--- a/src/i_system.c
+++ b/src/i_system.c
@@ -327,9 +327,9 @@
     
     // Message first.
     va_start(argptr, error);
-    fprintf(stderr, "\nError: ");
+    //fprintf(stderr, "\nError: ");
     vfprintf(stderr, error, argptr);
-    fprintf(stderr, "\n");
+    fprintf(stderr, "\n\n");
     va_end(argptr);
     fflush(stderr);
 
@@ -362,7 +362,7 @@
                             msgbuf, strlen(msgbuf) + 1,
                             wmsgbuf, sizeof(wmsgbuf));
 
-        MessageBoxW(NULL, wmsgbuf, L"Error", MB_OK);
+        MessageBoxW(NULL, wmsgbuf, L"", MB_OK);
     }
 #endif
 
--- a/src/net_gui.c
+++ b/src/net_gui.c
@@ -287,7 +287,7 @@
 
         if (!net_client_connected)
         {
-            I_Error("Disconnected from server");
+            I_Error("Lost connection to server");
         }
 
         TXT_Sleep(100);
--- a/src/net_query.c
+++ b/src/net_query.c
@@ -39,7 +39,7 @@
 
 // DNS address of the Internet master server.
 
-#define MASTER_SERVER_ADDRESS "master.chocolate-doom.org"
+#define MASTER_SERVER_ADDRESS "master.chocolate-doom.org:2342"
 
 // Time to wait for a response before declaring a timeout.
 
--- a/src/net_server.c
+++ b/src/net_server.c
@@ -70,6 +70,11 @@
     int last_send_time;
     char *name;
 
+    // Time that this client connected to the server.
+    // This is used to determine the controller (oldest client).
+
+    unsigned int connect_time;
+
     // Last time new gamedata was received from this client
     
     int last_gamedata_time;
@@ -382,19 +387,29 @@
 
 static net_client_t *NET_SV_Controller(void)
 {
+    net_client_t *best;
     int i;
 
-    // first client in the list is the controller
+    // Find the oldest client (first to connect).
 
+    best = NULL;
+
     for (i=0; i<MAXNETNODES; ++i)
     {
-        if (ClientConnected(&clients[i]) && !clients[i].drone)
+        // Can't be controller?
+
+        if (!ClientConnected(&clients[i]) || clients[i].drone)
         {
-            return &clients[i];
+            continue;
         }
+
+        if (best == NULL || clients[i].connect_time < best->connect_time)
+        {
+            best = &clients[i];
+        }
     }
 
-    return NULL;
+    return best;
 }
 
 // Given an address, find the corresponding client
@@ -434,6 +449,7 @@
                                  char *player_name)
 {
     client->active = true;
+    client->connect_time = I_GetTimeMS();
     NET_Conn_InitServer(&client->connection, addr);
     client->addr = addr;
     client->last_send_time = -1;
--- a/src/setup/mainmenu.c
+++ b/src/setup/mainmenu.c
@@ -189,6 +189,7 @@
 {
     txt_window_t *window;
     txt_window_action_t *quit_action;
+    txt_window_action_t *warp_action;
 
     window = TXT_NewWindow("Main Menu");
 
@@ -230,8 +231,12 @@
           NULL);
 
     quit_action = TXT_NewWindowAction(KEY_ESCAPE, "Quit");
+    warp_action = TXT_NewWindowAction(KEY_F1, "Warp");
     TXT_SignalConnect(quit_action, "pressed", QuitConfirm, NULL);
+    TXT_SignalConnect(warp_action, "pressed",
+                      (TxtWidgetSignalFunc) WarpMenu, NULL);
     TXT_SetWindowAction(window, TXT_HORIZ_LEFT, quit_action);
+    TXT_SetWindowAction(window, TXT_HORIZ_CENTER, warp_action);
 
     TXT_SetKeyListener(window, MainMenuKeyPress, NULL);
 }
--- a/src/setup/multiplayer.c
+++ b/src/setup/multiplayer.c
@@ -209,7 +209,11 @@
     }
 }
 
-static void StartGame(TXT_UNCAST_ARG(widget), TXT_UNCAST_ARG(user_data))
+// Callback function invoked to launch the game.
+// This is used when starting a server and also when starting a
+// single player game via the "warp" menu.
+
+static void StartGame(int multiplayer)
 {
     execute_context_t *exec;
 
@@ -221,7 +225,6 @@
     AddExtraParameters(exec);
 
     AddIWADParameter(exec);
-    AddCmdLineParameter(exec, "-server");
     AddCmdLineParameter(exec, "-skill %i", skill + 1);
 
     if (gamemission == hexen)
@@ -244,20 +247,6 @@
         AddCmdLineParameter(exec, "-respawn");
     }
 
-    if (deathmatch == 1)
-    {
-        AddCmdLineParameter(exec, "-deathmatch");
-    }
-    else if (deathmatch == 2)
-    {
-        AddCmdLineParameter(exec, "-altdeath");
-    }
-
-    if (timer > 0)
-    {
-        AddCmdLineParameter(exec, "-timer %i", timer);
-    }
-
     if (warptype == WARP_ExMy)
     {
         // TODO: select IWAD based on warp type
@@ -268,8 +257,28 @@
         AddCmdLineParameter(exec, "-warp %i", warpmap);
     }
 
-    AddCmdLineParameter(exec, "-port %i", udpport);
+    // Multiplayer-specific options:
 
+    if (multiplayer)
+    {
+        AddCmdLineParameter(exec, "-server");
+        AddCmdLineParameter(exec, "-port %i", udpport);
+
+        if (deathmatch == 1)
+        {
+            AddCmdLineParameter(exec, "-deathmatch");
+        }
+        else if (deathmatch == 2)
+        {
+            AddCmdLineParameter(exec, "-altdeath");
+        }
+
+        if (timer > 0)
+        {
+            AddCmdLineParameter(exec, "-timer %i", timer);
+        }
+    }
+
     AddWADs(exec);
 
     TXT_Shutdown();
@@ -282,6 +291,16 @@
     exit(0);
 }
 
+static void StartServerGame(TXT_UNCAST_ARG(widget), TXT_UNCAST_ARG(unused))
+{
+    StartGame(1);
+}
+
+static void StartSinglePlayerGame(TXT_UNCAST_ARG(widget), TXT_UNCAST_ARG(unused))
+{
+    StartGame(0);
+}
+
 static void UpdateWarpButton(void)
 {
     char buf[10];
@@ -544,13 +563,28 @@
     return result;
 }
 
-static txt_window_action_t *StartGameAction(void)
+// Create the window action button to start the game.  This invokes
+// a different callback depending on whether to start a multiplayer
+// or single player game.
+
+static txt_window_action_t *StartGameAction(int multiplayer)
 {
     txt_window_action_t *action;
+    TxtWidgetSignalFunc callback;
 
     action = TXT_NewWindowAction(KEY_F10, "Start");
-    TXT_SignalConnect(action, "pressed", StartGame, NULL);
 
+    if (multiplayer)
+    {
+        callback = StartServerGame;
+    }
+    else
+    {
+        callback = StartSinglePlayerGame;
+    }
+
+    TXT_SignalConnect(action, "pressed", callback, NULL);
+
     return action;
 }
 
@@ -591,7 +625,11 @@
     return action;
 }
 
-void StartMultiGame(void)
+// "Start game" menu.  This is used for the start server window
+// and the single player warp menu.  The parameters specify
+// the window title and whether to display multiplayer options.
+
+static void StartGameMenu(char *window_title, int multiplayer)
 {
     txt_window_t *window;
     txt_table_t *gameopt_table;
@@ -599,7 +637,7 @@
     txt_widget_t *iwad_selector;
     int num_mult_types = 2;
 
-    window = TXT_NewWindow("Start multiplayer game");
+    window = TXT_NewWindow(window_title);
 
     TXT_AddWidgets(window, 
                    gameopt_table = TXT_NewTable(2),
@@ -614,7 +652,7 @@
                    NULL);
 
     TXT_SetWindowAction(window, TXT_HORIZ_CENTER, WadWindowAction());
-    TXT_SetWindowAction(window, TXT_HORIZ_RIGHT, StartGameAction());
+    TXT_SetWindowAction(window, TXT_HORIZ_RIGHT, StartGameAction(multiplayer));
     
     TXT_SetColumnWidths(gameopt_table, 12, 12);
 
@@ -632,14 +670,8 @@
            iwad_selector = IWADSelector(),
            TXT_NewLabel("Skill"),
            skillbutton = TXT_NewDropdownList(&skill, doom_skills, 5),
-           TXT_NewLabel("Game type"),
-           TXT_NewDropdownList(&deathmatch, gamemodes, num_mult_types),
            TXT_NewLabel("Level warp"),
            warpbutton = TXT_NewButton2("????", LevelSelectDialog, NULL),
-           TXT_NewLabel("Time limit"),
-           TXT_NewHorizBox(TXT_NewIntInputBox(&timer, 2),
-                           TXT_NewLabel("minutes"),
-                           NULL),
            NULL);
 
     if (gamemission == hexen)
@@ -651,17 +683,39 @@
                        NULL);
     }
 
+    if (multiplayer)
+    {
+        TXT_AddWidgets(gameopt_table,
+               TXT_NewLabel("Game type"),
+               TXT_NewDropdownList(&deathmatch, gamemodes, num_mult_types),
+               TXT_NewLabel("Time limit"),
+               TXT_NewHorizBox(TXT_NewIntInputBox(&timer, 2),
+                               TXT_NewLabel("minutes"),
+                               NULL),
+               NULL);
+
+        TXT_AddWidgets(advanced_table, 
+                       TXT_NewLabel("UDP port"),
+                       TXT_NewIntInputBox(&udpport, 5),
+                       NULL);
+    }
+
     TXT_SetColumnWidths(advanced_table, 12, 12);
 
     TXT_SignalConnect(iwad_selector, "changed", UpdateWarpType, NULL);
 
-    TXT_AddWidgets(advanced_table, 
-                   TXT_NewLabel("UDP port"),
-                   TXT_NewIntInputBox(&udpport, 5),
-                   NULL);
-
     UpdateWarpType(NULL, NULL);
     UpdateWarpButton();
+}
+
+void StartMultiGame(void)
+{
+    StartGameMenu("Start multiplayer game", 1);
+}
+
+void WarpMenu(void)
+{
+    StartGameMenu("Level Warp", 0);
 }
 
 static void DoJoinGame(void *unused1, void *unused2)
--- a/src/setup/multiplayer.h
+++ b/src/setup/multiplayer.h
@@ -23,6 +23,7 @@
 #define SETUP_MULTIPLAYER_H
 
 void StartMultiGame(void);
+void WarpMenu(void);
 void JoinMultiGame(void);
 void MultiplayerConfig(void);
 
--- a/src/setup/txt_joybinput.c
+++ b/src/setup/txt_joybinput.c
@@ -206,6 +206,7 @@
 
 txt_widget_class_t txt_joystick_input_class =
 {
+    TXT_AlwaysSelectable,
     TXT_JoystickInputSizeCalc,
     TXT_JoystickInputDrawer,
     TXT_JoystickInputKeyPress,
--- a/src/setup/txt_keyinput.c
+++ b/src/setup/txt_keyinput.c
@@ -171,6 +171,7 @@
 
 txt_widget_class_t txt_key_input_class =
 {
+    TXT_AlwaysSelectable,
     TXT_KeyInputSizeCalc,
     TXT_KeyInputDrawer,
     TXT_KeyInputKeyPress,
--- a/src/setup/txt_mouseinput.c
+++ b/src/setup/txt_mouseinput.c
@@ -164,6 +164,7 @@
 
 txt_widget_class_t txt_mouse_input_class =
 {
+    TXT_AlwaysSelectable,
     TXT_MouseInputSizeCalc,
     TXT_MouseInputDrawer,
     TXT_MouseInputKeyPress,
--- a/textscreen/examples/guitest.c
+++ b/textscreen/examples/guitest.c
@@ -163,8 +163,8 @@
 {
     txt_window_t *window;
     txt_table_t *table;
+    txt_table_t *unselectable_table;
     txt_scrollpane_t *scrollpane;
-    int i;
 
     window = TXT_NewWindow("Another test");
     TXT_SetWindowPosition(window, 
@@ -172,10 +172,13 @@
                           TXT_VERT_TOP, 
                           TXT_SCREEN_W - 1, 1);
 
-    for (i=0; i<5; ++i)
-    {
-        TXT_AddWidget(window, TXT_NewButton("hello there blah blah blah blah"));
-    }
+    TXT_AddWidgets(window,
+                   TXT_NewScrollPane(40, 1,
+                        TXT_NewLabel("* Unselectable scroll pane *")),
+                   unselectable_table = TXT_NewTable(1),
+                   NULL);
+
+    TXT_AddWidget(unselectable_table, TXT_NewLabel("* Unselectable table *"));
 
     TXT_AddWidget(window, TXT_NewSeparator("Input boxes"));
     table = TXT_NewTable(2);
--- a/textscreen/txt_button.c
+++ b/textscreen/txt_button.c
@@ -96,6 +96,7 @@
 
 txt_widget_class_t txt_button_class =
 {
+    TXT_AlwaysSelectable,
     TXT_ButtonSizeCalc,
     TXT_ButtonDrawer,
     TXT_ButtonKeyPress,
--- a/textscreen/txt_checkbox.c
+++ b/textscreen/txt_checkbox.c
@@ -117,6 +117,7 @@
 
 txt_widget_class_t txt_checkbox_class =
 {
+    TXT_AlwaysSelectable,
     TXT_CheckBoxSizeCalc,
     TXT_CheckBoxDrawer,
     TXT_CheckBoxKeyPress,
--- a/textscreen/txt_dropdown.c
+++ b/textscreen/txt_dropdown.c
@@ -262,6 +262,7 @@
 
 txt_widget_class_t txt_dropdown_list_class =
 {
+    TXT_AlwaysSelectable,
     TXT_DropdownListSizeCalc,
     TXT_DropdownListDrawer,
     TXT_DropdownListKeyPress,
--- a/textscreen/txt_inputbox.c
+++ b/textscreen/txt_inputbox.c
@@ -232,6 +232,7 @@
 
 txt_widget_class_t txt_inputbox_class =
 {
+    TXT_AlwaysSelectable,
     TXT_InputBoxSizeCalc,
     TXT_InputBoxDrawer,
     TXT_InputBoxKeyPress,
@@ -242,6 +243,7 @@
 
 txt_widget_class_t txt_int_inputbox_class =
 {
+    TXT_AlwaysSelectable,
     TXT_InputBoxSizeCalc,
     TXT_InputBoxDrawer,
     TXT_IntInputBoxKeyPress,
--- a/textscreen/txt_label.c
+++ b/textscreen/txt_label.c
@@ -104,6 +104,7 @@
 
 txt_widget_class_t txt_label_class =
 {
+    TXT_NeverSelectable,
     TXT_LabelSizeCalc,
     TXT_LabelDrawer,
     NULL,
@@ -170,7 +171,6 @@
     label = malloc(sizeof(txt_label_t));
 
     TXT_InitWidget(label, &txt_label_class);
-    label->widget.selectable = 0;
     label->label = NULL;
     label->lines = NULL;
 
--- a/textscreen/txt_radiobutton.c
+++ b/textscreen/txt_radiobutton.c
@@ -121,6 +121,7 @@
 
 txt_widget_class_t txt_radiobutton_class =
 {
+    TXT_AlwaysSelectable,
     TXT_RadioButtonSizeCalc,
     TXT_RadioButtonDrawer,
     TXT_RadioButtonKeyPress,
--- a/textscreen/txt_scrollpane.c
+++ b/textscreen/txt_scrollpane.c
@@ -138,7 +138,7 @@
     }
     if (scrollpane->expand_h)
     {
-        scrollpane->h = FullWidth(scrollpane);
+        scrollpane->h = FullHeight(scrollpane);
     }
 
     scrollpane->widget.w = scrollpane->w;
@@ -486,8 +486,26 @@
     }
 }
 
+static int TXT_ScrollPaneSelectable(TXT_UNCAST_ARG(scrollpane))
+{
+    TXT_CAST_ARG(txt_scrollpane_t, scrollpane);
+
+    // If scroll bars are displayed, the scroll pane must be selectable
+    // so that we can use the arrow keys to scroll around.
+
+    if (NeedsScrollbars(scrollpane))
+    {
+        return 1;
+    }
+
+    // Otherwise, whether this is selectable depends on the child widget.
+
+    return TXT_SelectableWidget(scrollpane->child);
+}
+
 txt_widget_class_t txt_scrollpane_class =
 {
+    TXT_ScrollPaneSelectable,
     TXT_ScrollPaneSizeCalc,
     TXT_ScrollPaneDrawer,
     TXT_ScrollPaneKeyPress,
--- a/textscreen/txt_separator.c
+++ b/textscreen/txt_separator.c
@@ -82,6 +82,7 @@
 
 txt_widget_class_t txt_separator_class =
 {
+    TXT_NeverSelectable,
     TXT_SeparatorSizeCalc,
     TXT_SeparatorDrawer,
     NULL,
@@ -97,7 +98,6 @@
     separator = malloc(sizeof(txt_separator_t));
 
     TXT_InitWidget(separator, &txt_separator_class);
-    separator->widget.selectable = 0;
 
     if (label != NULL)
     {
--- a/textscreen/txt_spinctrl.c
+++ b/textscreen/txt_spinctrl.c
@@ -358,6 +358,7 @@
 
 txt_widget_class_t txt_spincontrol_class =
 {
+    TXT_AlwaysSelectable,
     TXT_SpinControlSizeCalc,
     TXT_SpinControlDrawer,
     TXT_SpinControlKeyPress,
--- a/textscreen/txt_strut.c
+++ b/textscreen/txt_strut.c
@@ -55,6 +55,7 @@
 
 txt_widget_class_t txt_strut_class =
 {
+    TXT_NeverSelectable,
     TXT_StrutSizeCalc,
     TXT_StrutDrawer,
     TXT_StrutKeyPress,
@@ -70,7 +71,6 @@
     strut = malloc(sizeof(txt_strut_t));
 
     TXT_InitWidget(strut, &txt_strut_class);
-    strut->widget.selectable = 0;
     strut->width = width;
     strut->height = height;
 
--- a/textscreen/txt_table.c
+++ b/textscreen/txt_table.c
@@ -202,7 +202,7 @@
     va_end(args);
 }
 
-static int SelectableWidget(txt_table_t *table, int x, int y)
+static int SelectableCell(txt_table_t *table, int x, int y)
 {
     txt_widget_t *widget;
     int i;
@@ -217,7 +217,9 @@
     if (i >= 0 && i < table->num_widgets)
     {
         widget = table->widgets[i];
-        return widget != NULL && widget->selectable && widget->visible;
+        return widget != NULL
+            && TXT_SelectableWidget(widget)
+            && widget->visible;
     }
 
     return 0;
@@ -237,7 +239,7 @@
     {
         // Search to the right
 
-        if (SelectableWidget(table, start_col + x, row))
+        if (SelectableCell(table, start_col + x, row))
         {
             return start_col + x;
         }
@@ -244,7 +246,7 @@
 
         // Search to the left
 
-        if (SelectableWidget(table, start_col - x, row))
+        if (SelectableCell(table, start_col - x, row))
         {
             return start_col - x;
         }
@@ -270,7 +272,7 @@
     if (selected >= 0 && selected < table->num_widgets)
     {
         if (table->widgets[selected] != NULL
-         && table->widgets[selected]->selectable
+         && TXT_SelectableWidget(table->widgets[selected])
          && TXT_WidgetKeyPress(table->widgets[selected], key))
         {
             return 1;
@@ -329,7 +331,7 @@
 
         for (new_x = table->selected_x - 1; new_x >= 0; --new_x)
         {
-            if (SelectableWidget(table, new_x, table->selected_y))
+            if (SelectableCell(table, new_x, table->selected_y))
             {
                 // Found a selectable widget!
 
@@ -348,7 +350,7 @@
 
         for (new_x = table->selected_x + 1; new_x < table->columns; ++new_x)
         {
-            if (SelectableWidget(table, new_x, table->selected_y))
+            if (SelectableCell(table, new_x, table->selected_y))
             {
                 // Found a selectable widget!
 
@@ -547,7 +549,7 @@
 
                 // Select the cell if the widget is selectable
 
-                if (widget->selectable)
+                if (TXT_SelectableWidget(widget))
                 {
                     table->selected_x = i % table->columns;
                     table->selected_y = i / table->columns;
@@ -563,8 +565,41 @@
     }
 }
 
+// Determine whether the table is selectable.
+
+static int TXT_TableSelectable(TXT_UNCAST_ARG(table))
+{
+    TXT_CAST_ARG(txt_table_t, table);
+    int i;
+
+    // Is the currently-selected cell selectable?
+
+    if (SelectableCell(table, table->selected_x, table->selected_y))
+    {
+        return 1;
+    }
+
+    // Find the first selectable cell and set selected_x, selected_y.
+
+    for (i = 0; i < table->num_widgets; ++i)
+    {
+        if (table->widgets[i] != NULL
+         && TXT_SelectableWidget(table->widgets[i]))
+        {
+            table->selected_x = i % table->columns;
+            table->selected_y = i / table->columns;
+            return 1;
+        }
+    }
+
+    // No selectable widgets exist within the table.
+
+    return 0;
+}
+
 txt_widget_class_t txt_table_class =
 {
+    TXT_TableSelectable,
     TXT_CalcTableSize,
     TXT_TableDrawer,
     TXT_TableKeyPress,
--- a/textscreen/txt_widget.c
+++ b/textscreen/txt_widget.c
@@ -83,9 +83,8 @@
     widget->widget_class = widget_class;
     widget->callback_table = TXT_NewCallbackTable();
 
-    // Default values: visible and selectable
+    // Visible by default.
 
-    widget->selectable = 1;
     widget->visible = 1;
 
     // Align left by default
@@ -211,6 +210,30 @@
     if (widget->widget_class->layout != NULL)
     {
         widget->widget_class->layout(widget);
+    }
+}
+
+int TXT_AlwaysSelectable(TXT_UNCAST_ARG(widget))
+{
+    return 1;
+}
+
+int TXT_NeverSelectable(TXT_UNCAST_ARG(widget))
+{
+    return 0;
+}
+
+int TXT_SelectableWidget(TXT_UNCAST_ARG(widget))
+{
+    TXT_CAST_ARG(txt_widget_t, widget);
+
+    if (widget->widget_class->selectable != NULL)
+    {
+        return widget->widget_class->selectable(widget);
+    }
+    else
+    {
+        return 0;
     }
 }
 
--- a/textscreen/txt_widget.h
+++ b/textscreen/txt_widget.h
@@ -77,9 +77,11 @@
 typedef void (*TxtWidgetSignalFunc)(TXT_UNCAST_ARG(widget), void *user_data);
 typedef void (*TxtMousePressFunc)(TXT_UNCAST_ARG(widget), int x, int y, int b);
 typedef void (*TxtWidgetLayoutFunc)(TXT_UNCAST_ARG(widget));
+typedef int (*TxtWidgetSelectableFunc)(TXT_UNCAST_ARG(widget));
 
 struct txt_widget_class_s
 {
+    TxtWidgetSelectableFunc selectable;
     TxtWidgetSizeCalc size_calc;
     TxtWidgetDrawer drawer;
     TxtWidgetKeyPress key_press;
@@ -92,7 +94,6 @@
 {
     txt_widget_class_t *widget_class;
     txt_callback_table_t *callback_table;
-    int selectable;
     int visible;
     txt_horiz_align_t align;
 
@@ -111,6 +112,8 @@
 void TXT_WidgetMousePress(TXT_UNCAST_ARG(widget), int x, int y, int b);
 void TXT_DestroyWidget(TXT_UNCAST_ARG(widget));
 void TXT_LayoutWidget(TXT_UNCAST_ARG(widget));
+int TXT_AlwaysSelectable(TXT_UNCAST_ARG(widget));
+int TXT_NeverSelectable(TXT_UNCAST_ARG(widget));
 
 /**
  * Set a callback function to be invoked when a signal occurs.
@@ -133,6 +136,15 @@
  */
 
 void TXT_SetWidgetAlign(TXT_UNCAST_ARG(widget), txt_horiz_align_t horiz_align);
+
+/**
+ * Query whether a widget is selectable with the cursor.
+ *
+ * @param widget       The widget.
+ * @return             Non-zero if the widget is selectable.
+ */
+
+int TXT_SelectableWidget(TXT_UNCAST_ARG(widget));
 
 #endif /* #ifndef TXT_WIDGET_H */
 
--- a/textscreen/txt_window.c
+++ b/textscreen/txt_window.c
@@ -140,7 +140,16 @@
 static void LayoutActionArea(txt_window_t *window)
 {
     txt_widget_t *widget;
+    int space_available;
+    int space_left_offset;
 
+    // We need to calculate the available horizontal space for the center
+    // action widget, so that we can center it within it.
+    // To start with, we have the entire action area available.
+
+    space_available = window->window_w;
+    space_left_offset = 0;
+
     // Left action
 
     if (window->actions[TXT_HORIZ_LEFT] != NULL)
@@ -151,29 +160,43 @@
 
         widget->x = window->window_x + 2;
         widget->y = window->window_y + window->window_h - widget->h - 1;
+
+        // Adjust available space:
+
+        space_available -= widget->w;
+        space_left_offset += widget->w;
     }
 
-    // Draw the center action
+    // Draw the right action
 
-    if (window->actions[TXT_HORIZ_CENTER] != NULL)
+    if (window->actions[TXT_HORIZ_RIGHT] != NULL)
     {
-        widget = (txt_widget_t *) window->actions[TXT_HORIZ_CENTER];
+        widget = (txt_widget_t *) window->actions[TXT_HORIZ_RIGHT];
 
         TXT_CalcWidgetSize(widget);
 
-        widget->x = window->window_x + (window->window_w - widget->w - 2) / 2;
+        widget->x = window->window_x + window->window_w - 2 - widget->w;
         widget->y = window->window_y + window->window_h - widget->h - 1;
+
+        // Adjust available space:
+
+        space_available -= widget->w;
     }
 
-    // Draw the right action
+    // Draw the center action
 
-    if (window->actions[TXT_HORIZ_RIGHT] != NULL)
+    if (window->actions[TXT_HORIZ_CENTER] != NULL)
     {
-        widget = (txt_widget_t *) window->actions[TXT_HORIZ_RIGHT];
+        widget = (txt_widget_t *) window->actions[TXT_HORIZ_CENTER];
 
         TXT_CalcWidgetSize(widget);
 
-        widget->x = window->window_x + window->window_w - 2 - widget->w;
+        // The left and right widgets have left a space sandwiched between
+        // them.  Center this widget within that space.
+
+        widget->x = window->window_x
+                  + space_left_offset
+                  + (space_available - widget->w) / 2;
         widget->y = window->window_y + window->window_h - widget->h - 1;
     }
 }
--- a/textscreen/txt_window_action.c
+++ b/textscreen/txt_window_action.c
@@ -93,6 +93,7 @@
 
 txt_widget_class_t txt_window_action_class =
 {
+    TXT_AlwaysSelectable,
     TXT_WindowActionSizeCalc,
     TXT_WindowActionDrawer,
     TXT_WindowActionKeyPress,