/* Copyright 1998 Acorn Computers Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* CLib */
#include "kernel.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Toolbox */
#include "event.h"
#include "toolbox.h"
#include "window.h"
#include "gadgets.h"
#include "ScrollList.h"
#include "wimplib.h"
/* Common */
#include "misc.h"
/* local headers */
#include "Main.h"
#include "Ids.h"
#include "ToolboxE.h"  /* includes prototypes for this file */

/* These should probably be picked up from a master header...  */
#define OS_SWINumberFromString	((int) 0x39)
#define OS_ReadMonotonicTime	((int) 0x42)
#define OS_ReadModeVariable	((int) 0x35)

#define UNMAPPED_TEXT		"Unmapped"

/* USBJoystick's API (start) */
#define JOY_NOT_MAPPED		((int) -1000)

#define MAP_LEGACY_STICK	((int) 0)
#define MAP_AXES8_X		((int) 1)
#define MAP_AXES8_Y		((int) 2)
#define MAP_AXES16_X		((int) 3)
#define MAP_AXES16_Y		((int) 4)

#define JOYSTICK_EMULATE_SERIALPORT	((int) 0)
#define JOYSTICK_EMULATE_JOY		((int) 1)
#define JOYSTICK_EMULATE_ADC		((int) 2)
#define JOYSTICK_CONTROL_MOUSE		((int) 0)
#define JOYSTICK_CONTROL_KEYBOARD	((int) 1)

#define JB_SRC_AXIS		0
#define JB_SRC_BUTTON		1

#define JB_TGT_SLOT_AXIS	1
#define JB_TGT_SLOT_BTN		2
#define JB_TGT_MOUSE_MOVE	3
#define JB_TGT_MOUSE_BTN	4
#define JB_TGT_KEY		5
#define JB_TGT_ADC		6
#define JB_TGT_ADC_BTN		7

#define JB_AXIS_8X		0
#define JB_AXIS_8Y		1
#define JB_AXIS_16X		2
#define JB_AXIS_16Y		3
#define JB_MOUSE_X		0
#define JB_MOUSE_Y		1
#define JB_MOUSE_SELECT		0
#define JB_MOUSE_MENU		1
#define JB_MOUSE_ADJUST		2

#define JB_FLAG_INVERT		0x01
#define JB_MAX_BINDINGS		128

typedef struct {
  unsigned char  source_type;
  unsigned char  source_index;
  unsigned char  target_kind;
  unsigned char  flags;
  unsigned short target_index;
  unsigned short reserved;
  unsigned int   param;
} joy_binding;

struct device_info
{
  unsigned int size;
  char *manufacturer, *product, *serial;
  unsigned int vendor_id, product_id;
  unsigned int num_axes, num_buttons, num_hats;
  unsigned int capabilities;
  unsigned char guid [16];
};

struct axis_info
{
  unsigned int hid_usage_page, hid_usage, type;
  char *name;
  int min, mid, max;
  int is_digital;
};

/* USBJoystick's API (end) */



#define POLL_INTERVAL		((int) 10)

#define RESCAN_INTERVAL		((int) 300)	/* ~3 seconds */

/* Device/slot buttons */
#define BUTTON_GRID_COLS	((int) 16)
#define BUTTON_GRID_ROW_GAP	((int) 64)

/* Stick buttons: slightly less room to work with here */
#define STICK_BUTTON_COL_GAP	((int) 16)
#define DEV_BUTTON_COL_GAP	((int) 32)

static int next_poll_due = 0;
static int next_rescan_due = 0;

/* A master-list row / an open detail window is one of these two kinds */
#define KIND_STICK	0
#define KIND_DEVICE	1

static int toolboxe_objectautocreated (int event_code, ToolboxEvent *event, IdBlock *id_block, void *handle);
static int toolboxe_scrolllistselection (int event_code, ToolboxEvent *event, IdBlock *id_block, void *handle);
static int toolboxe_actionbuttonselected (int event_code, ToolboxEvent *event, IdBlock *id_block, void *handle);
static int toolboxe_stringsetchanged (int event_code, ToolboxEvent *event, IdBlock *id_block, void *handle);
static int toolboxe_windowhidden (int event_code, ToolboxEvent *event, IdBlock *id_block, void *handle);

int toolboxe_events [6] =   { Toolbox_ObjectAutoCreated,
				ScrollList_Selection,
				ActionButton_Selected,
				StringSet_ValueChanged,
				Window_HasBeenHidden,
				0 };

/* Whether each API tier answered, and the resolved SWI numbers
   (probed once, reused on every poll) */
static BOOL have_stick_read       = FALSE;
static BOOL have_stick_enumerate  = FALSE;
static BOOL have_deviceinfo       = FALSE;
static BOOL have_axisinfo         = FALSE;
static BOOL have_axisvalues       = FALSE;
static BOOL have_getmapping       = FALSE;
static BOOL have_setstick         = FALSE;
static BOOL have_readbindings     = FALSE;
static BOOL have_writebindings    = FALSE;
static int  stick_read_swi        = 0;
static int  enumerate_swi         = 0;
static int  deviceinfo_swi        = 0;
static int  axisinfo_swi          = 0;
static int  axisvalues_swi        = 0;
static int  getmapping_swi        = 0;
static int  setstick_swi          = 0;
static int  readbindings_swi      = 0;
static int  writebindings_swi     = 0;
static BOOL have_readdefaultbindings = FALSE;
static int  readdefaultbindings_swi  = 0;
static BOOL have_save             = FALSE;
static int  save_swi              = 0;

/* Master window's global Emulation/Control settings */
static BOOL have_emulate          = FALSE;
static int  emulate_swi           = 0;
static BOOL have_control          = FALSE;
static int  control_swi           = 0;
static BOOL have_savesettings     = FALSE;
static int  savesettings_swi      = 0;

/* Master list */
struct list_row { int kind; int index; };
static struct list_row row_map [STICK_MAX + DEVICE_SLOT_MAX];
static int  row_count            = 0;
static BOOL list_built           = FALSE;
static unsigned int enum_generation = 0;
static int  sel_kind             = -1;
static int  sel_index            = 0;

/* Open detail windows. obj == NULL_ObjectId marks a free slot. */
struct detail_win
{
  ObjectId obj; int kind; int index; unsigned int last_buttons;
  joy_binding draft_bindings [JB_MAX_BINDINGS];
  int         draft_count;
  int         draft_stick;
};
static struct detail_win detail [MAX_DETAIL];

/* BindEdit dialogue state. One instance at a time, editing exactly ONE binding */
static ObjectId    bindedit_obj        = NULL_ObjectId;
static int         bindedit_slot       = -1;
static int         bindedit_src_type   = JB_SRC_AXIS;
static int         bindedit_src_index  = 0;
static joy_binding bindedit_table [JB_MAX_BINDINGS];
static int         bindedit_count      = 0;
static int         bindedit_edit_index = -1;		/* table slot being edited; -1 = adding */

void toolboxe_register (void)
{
  throw (event_register_toolbox_handler (-1, Toolbox_ObjectAutoCreated, toolboxe_objectautocreated, NULL));
  throw (event_register_toolbox_handler (-1, ScrollList_Selection, toolboxe_scrolllistselection, NULL));
  throw (event_register_toolbox_handler (-1, ActionButton_Selected, toolboxe_actionbuttonselected, NULL));
  throw (event_register_toolbox_handler (-1, StringSet_ValueChanged, toolboxe_stringsetchanged, NULL));
  throw (event_register_toolbox_handler (-1, Window_HasBeenHidden, toolboxe_windowhidden, NULL));
}

/******	probe_swi() ***********************************************\

Purpose:	Finds out whether a named SWI exists, without calling it -
		OS_SWINumberFromString just resolves the name against the
		currently loaded modules' SWI tables. The "try it and see"
		convention: no module name/version check, just ask whether
		the SWI itself is there.

\**************************************************************************/

static BOOL probe_swi (const char *name, int *swinum)
{
  _kernel_swi_regs regs;
  regs.r[1] = (int) name;
  if (_kernel_swi (OS_SWINumberFromString, &regs, &regs) != NULL) return FALSE;
  *swinum = regs.r[0];
  return TRUE;
}

/******	read_stick() **********************************************\

Purpose:	Calls Joystick_Read for one stick number/reason code.
		Returns FALSE if Joystick_Read itself isn't present, or if
		this particular reason code errors. Per the PRM an unsupported
		provider answering with default/centred data instead of an
		error can't be told apart from a real centred stick.

\**************************************************************************/

static BOOL read_stick (int stick, int reason, int *out_r0, int *out_r1)
{
  _kernel_swi_regs regs;
  if (!have_stick_read) return FALSE;
  regs.r[0] = (stick & 0xFF) | (reason << 8);
  if (_kernel_swi (stick_read_swi, &regs, &regs) != NULL) return FALSE;
  *out_r0 = regs.r[0];
  *out_r1 = regs.r[1];
  return TRUE;
}

/******	device_call() *********************************************\

Purpose:	Calls one of the DeviceInfo/AxisInfo/AxisValues/GetMapping
		SWIs for a slot - R0 on entry = slot. Any of r1-r4 can be
		NULL if that register isn't needed.

\**************************************************************************/

static BOOL device_call (int swi, int slot, int *r1, int *r2, int *r3, int *r4)
{
  _kernel_swi_regs regs;
  regs.r[0] = slot;
  if (_kernel_swi (swi, &regs, &regs) != NULL) return FALSE;
  if (r1) *r1 = regs.r[1];
  if (r2) *r2 = regs.r[2];
  if (r3) *r3 = regs.r[3];
  if (r4) *r4 = regs.r[4];
  return TRUE;
}

/******	fade_gadget() *********************************************\

Purpose:	Sets or clears Gadget_Faded on one gadget of one window.

\**************************************************************************/

static void fade_gadget (ObjectId win, ComponentId c, BOOL fade)
{
  unsigned int flags;
  if (gadget_get_flags (0, win, c, &flags) != NULL) return;
  if (fade) flags |= Gadget_Faded; else flags &= ~Gadget_Faded;
  gadget_set_flags (0, win, c, flags);
}

/******	footer_pane() *********************************************\

Purpose:	The DevDetail window's bottom tool-bar pane (its Default/Cancel/
		Set live there). Returned as its own ObjectId, or NULL_ObjectId
		if the window has no internal bottom-left tool bar. Used to
		address the footer's gadgets (they aren't on the main window).

\**************************************************************************/

static ObjectId footer_pane (ObjectId win)
{
  ObjectId ibl = NULL_ObjectId, itl, ebl, etl;
  if (window_get_tool_bars (0, win, &ibl, &itl, &ebl, &etl) != NULL) return NULL_ObjectId;
  return ibl;
}

/*========================================================================*/
/* Open-window tracker helpers						  */
/*========================================================================*/

static struct detail_win *find_detail_by_obj (ObjectId obj)
{
  int i;
  if (obj == NULL_ObjectId) return NULL;	/* never match a free slot (obj==0) */
  for (i = 0; i < MAX_DETAIL; i++)
    if (detail [i].obj == obj) return &detail [i];
  return NULL;
}

/* Resolve a DevDetail from its footer tool-bar pane's ObjectId. The pane is a
   separate object, so its button events carry the pane as self_id, not the
   DevDetail. Ask each open device window for its current bottom tool bar and
   match - queried live (via footer_pane below), since the pane isn't realised
   until the window is shown. */
static struct detail_win *find_detail_by_footer (ObjectId obj)
{
  int i;
  if (obj == NULL_ObjectId) return NULL;
  for (i = 0; i < MAX_DETAIL; i++)
    if (detail [i].obj != NULL_ObjectId && detail [i].kind == KIND_DEVICE &&
        footer_pane (detail [i].obj) == obj)
      return &detail [i];
  return NULL;
}

static struct detail_win *find_detail (int kind, int index)
{
  int i;
  for (i = 0; i < MAX_DETAIL; i++)
    if (detail [i].obj != NULL_ObjectId && detail [i].kind == kind && detail [i].index == index)
      return &detail [i];
  return NULL;
}

static struct detail_win *find_free_detail (void)
{
  int i;
  for (i = 0; i < MAX_DETAIL; i++)
    if (detail [i].obj == NULL_ObjectId) return &detail [i];
  return NULL;
}

/******	clone_grid() **********************************************\

Purpose:	Duplicates one gadget - laid out just once in ResEd - into a
		grid of `count` copies in the freshly-created (not-yet-shown)
		window `win`. Saves hand-building 32 near-identical buttons +
		labels: ResEd holds only component `base` (the top-left cell,
		fully configured), and this clones it right/down in `cols`
		columns, stepping by the template gadget's own size, giving
		component IDs base..base+count-1. If `number` is TRUE each
		clone's Label text (the char* right after the 36-byte
		GadgetHeader) is set to its index - for the button-number
		labels. Gadget 0 already exists from the template, so we only
		add 1..count-1.

		Offsets into the extracted gadget descriptor: box at +8 (BBox),
		component_id at +24 (see GadgetHeader in gadgets.h); Label's
		text pointer at +36 (Label = { char *label } after the header).
		The descriptor is copied before editing so the template stays
		intact; window_add_gadget copies any strings it points at, so
		the temporary number buffer is fine.

\**************************************************************************/

#define GADGET_BOX_OFF		8	/* GadgetHeader.box        */
#define GADGET_COMPID_OFF	24	/* GadgetHeader.component_id */
#define LABEL_TEXT_OFF		36	/* Label.label (first field after the header) */

static void clone_grid (ObjectId win, const char *tmpl, ComponentId base,
                        int count, int cols, ComponentId step_ref, BOOL number,
                        int col_gap)
{
  void *tp, *desc;
  int size, n, bw, bh, stepw, steph;
  char buf [256];
  BBox box0, sbox;

  /* Arg 3 is void** in older tboxlibs headers but const ObjectTemplateHeader**
     in current ones; pass via void* so it compiles cleanly against both. */
  if (toolbox_template_lookup (0, (char *) tmpl, (void *) &tp) != NULL) return;

  /* Cell step comes from the reference gadget (the button), NOT the gadget
     being cloned - so number labels stay aligned under their buttons even
     though a label is a different size from a button. */
  if (window_extract_gadget_info (0, (ObjectTemplateHeader *) tp, step_ref, &desc, &size) != NULL) return;
  sbox = *(BBox *) ((char *) desc + GADGET_BOX_OFF);
  stepw = sbox.xmax - sbox.xmin;
  steph = sbox.ymax - sbox.ymin;	/* positive; Y grows downward, so rows subtract */

  if (window_extract_gadget_info (0, (ObjectTemplateHeader *) tp, base, &desc, &size) != NULL) return;
  if (size <= 0 || size > (int) sizeof buf) return;
  memcpy (buf, desc, size);
  box0 = *(BBox *) (buf + GADGET_BOX_OFF);
  bw = box0.xmax - box0.xmin;		/* the clone keeps its OWN size... */
  bh = box0.ymax - box0.ymin;

  for (n = 1; n < count; n++)		/* ...but steps by the cell (button) size */
  {
    int col = n % cols, row = n / cols;
    char num [4];			/* must outlive the add_gadget call, which copies it */
    BBox *b = (BBox *) (buf + GADGET_BOX_OFF);
    b->xmin = box0.xmin + col * (stepw + col_gap);
    b->xmax = b->xmin + bw;
    b->ymax = box0.ymax - row * (steph + BUTTON_GRID_ROW_GAP);
    b->ymin = b->ymax - bh;
    *(ComponentId *) (buf + GADGET_COMPID_OFF) = base + n;
    if (number)
    {
      sprintf (num, "%d", n);
      *(char **) (buf + LABEL_TEXT_OFF) = num;
    }
    window_add_gadget (0, win, (Gadget *) buf, NULL);
  }
}

/*========================================================================*/
/* StickDetail - one legacy Joystick_Read stick				  */
/*========================================================================*/

/******	setup_stick_detail() **************************************\

Purpose:	One-time setup of a freshly-created StickDetail window for
		stick number 'stick' - just the fixed slider bounds; live
		values arrive on the first poll_stick_detail().

\**************************************************************************/

static void setup_stick_detail (ObjectId win, int stick)
{
  char title [24];
  int r0, r1, i;
  BOOL got16;

  /* flags=7 (bits 0/1/2 = lower/upper/step) - NumberRange_SetBounds only
     forwards the new bounds to its child Slider gadget (the thing that
     actually draws the bar) when the corresponding bit is set; its own
     copy, used only for the text, updates unconditionally regardless.
     flags=0 silently means "don't tell the slider" - see numrange.c. */
  numberrange_set_bounds (7, win, stickdetail_x8, -127, 127, 1, 0);
  numberrange_set_bounds (7, win, stickdetail_y8, -127, 127, 1, 0);
  numberrange_set_bounds (7, win, stickdetail_x16, 0, 65535, 1, 0);
  numberrange_set_bounds (7, win, stickdetail_y16, 0, 65535, 1, 0);

  /* The button indicators are read-only sprite Button gadgets (btnon/btnoff
     swapped via button_set_value in poll_stick_detail) - display-only, so no
     fading for interactivity. Only btn(0)/btnlabel(0) are in the Res; clone
     the rest here into a grid. */
  clone_grid (win, "StickDetail", stickdetail_btn (0),      STICK_BUTTON_MAX, BUTTON_GRID_COLS, stickdetail_btn (0), FALSE, STICK_BUTTON_COL_GAP);
  clone_grid (win, "StickDetail", stickdetail_btnlabel (0), STICK_BUTTON_MAX, BUTTON_GRID_COLS, stickdetail_btn (0), TRUE,  STICK_BUTTON_COL_GAP);

  /* Joystick_Read's 8-bit reason carries only 16 buttons; the full 32 need
     the 16-bit reason (R1). If this stick has no 16-bit read, fade buttons
     16..31 - they can never light. */
  got16 = read_stick (stick, 1, &r0, &r1);
  for (i = 16; i < STICK_BUTTON_MAX; i++)
    fade_gadget (win, stickdetail_btn (i), !got16);

  sprintf (title, "Stick %d", stick);
  window_set_title (0, win, title);
}

/******	poll_stick_detail() ***************************************\

Purpose:	Refreshes one open StickDetail's live 8-bit/16-bit axis
		values and button indicators from Joystick_Read.

\**************************************************************************/

static void poll_stick_detail (struct detail_win *d)
{
  ObjectId win = d->obj;
  int stick = d->index;
  int r0, r1, i;
  BOOL got16;
  unsigned int buttons = 0;
  BOOL have_buttons = FALSE;

  if (read_stick (stick, 0, &r0, &r1))
  {
    int x8 = (signed char) (r0 & 0xFF);
    int y8 = (signed char) ((r0 >> 8) & 0xFF);

    numberrange_set_value (0, win, stickdetail_x8, x8);
    numberrange_set_value (0, win, stickdetail_y8, y8);
    /* The 8-bit reason packs up to 16 buttons into bits 16-31 - a fallback
       used only if the 16-bit read (which carries the full 32 in R1) fails */
    buttons = ((unsigned int) r0 >> 16) & 0xFFFF;
    have_buttons = TRUE;
  }

  got16 = read_stick (stick, 1, &r0, &r1);
  fade_gadget (win, stickdetail_x16_label, !got16);
  fade_gadget (win, stickdetail_x16, !got16);
  fade_gadget (win, stickdetail_y16_label, !got16);
  fade_gadget (win, stickdetail_y16, !got16);
  if (!got16)
  {
    /* Faded doesn't clear the number - reset to the documented
       "centred/no stick" value rather than leave a stale reading */
    numberrange_set_value (0, win, stickdetail_x16, 32768);
    numberrange_set_value (0, win, stickdetail_y16, 32768);
  }
  else
  {
    int y16 = r0 & 0xFFFF;
    int x16 = (r0 >> 16) & 0xFFFF;
    numberrange_set_value (0, win, stickdetail_x16, x16);
    numberrange_set_value (0, win, stickdetail_y16, y16);
    /* R1 of the 16-bit read is the full button bitmask (up to 32) - preferred
       over the 8-bit read's 16, hence read second so it wins */
    buttons = (unsigned int) r1;
    have_buttons = TRUE;
  }

  if (have_buttons)
  {
    /* Only re-plot buttons whose state changed since last poll */
    for (i = 0; i < STICK_BUTTON_MAX; i++)
      if (((buttons ^ d->last_buttons) >> i) & 1)
        button_set_value (0, win, stickdetail_btn (i), ((buttons >> i) & 1) ? "btnon" : "btnoff");
    d->last_buttons = buttons;
  }
}

/*========================================================================*/
/* DevDetail - one enumerated USB slot					  */
/*========================================================================*/

/******	read_bindings() *******************************************\

Purpose:	Read a slot's binding table into `out` (up to `max` records).
		Returns the count (>= 0) on success, -1 if unavailable or the
		record size doesn't match our joy_binding (a version skew - bail
		rather than misread).

\**************************************************************************/

static int read_bindings (int slot, joy_binding *out, int max)
{
  _kernel_swi_regs regs;
  int count, recsize, ptr;

  if (!have_readbindings) return -1;
  regs.r[0] = slot;
  if (_kernel_swi (readbindings_swi, &regs, &regs) != NULL) return -1;
  count   = regs.r[0];
  recsize = regs.r[1];
  ptr     = regs.r[2];
  if (recsize != (int) sizeof (joy_binding) || count < 0) return -1;
  if (count > max) count = max;
  memcpy (out, (void *) ptr, count * sizeof (joy_binding));
  return count;
}

/******	read_default_bindings() ***********************************\

Purpose:	Read the default-mapping PREVIEW (Joystick_ReadDefaultBindings)
		into `out`/`out_stick` - a computed table, not live state, so
		unlike read_bindings() above this never touches the device or
		the config store. Backs the Default button: it resets the DRAFT
		to this, not the module's mapping. Same success/failure
		convention as read_bindings().

\**************************************************************************/

static int read_default_bindings (int slot, joy_binding *out, int max, int *out_stick)
{
  _kernel_swi_regs regs;
  int count, recsize, ptr;

  if (!have_readdefaultbindings) return -1;
  regs.r[0] = slot;
  if (_kernel_swi (readdefaultbindings_swi, &regs, &regs) != NULL) return -1;
  count   = regs.r[0];
  recsize = regs.r[1];
  ptr     = regs.r[2];
  if (recsize != (int) sizeof (joy_binding) || count < 0) return -1;
  if (count > max) count = max;
  memcpy (out, (void *) ptr, count * sizeof (joy_binding));
  if (out_stick) *out_stick = regs.r[3];
  return count;
}

/******	format_target() *******************************************\

Purpose:	Format one binding's TARGET into buf (the source is implied by
		context), e.g. "stick:16bitX", "mouse:Y", "key:57", with
		" (inv)" appended when the binding is inverted.

\**************************************************************************/

static void format_target (const joy_binding *b, char *buf)
{
  const char *t = "?";
  char detail [24];

  switch (b->target_kind)
  {
    case JB_TGT_SLOT_AXIS:
      switch (b->target_index) {
        case JB_AXIS_8X:  t = "stick:8bitX";  break;
        case JB_AXIS_8Y:  t = "stick:8bitY";  break;
        case JB_AXIS_16X: t = "stick:16bitX"; break;
        case JB_AXIS_16Y: t = "stick:16bitY"; break;
      }
      break;
    case JB_TGT_SLOT_BTN:   sprintf (detail, "stickbtn:%d", b->target_index); t = detail; break;
    case JB_TGT_MOUSE_MOVE: t = (b->target_index == JB_MOUSE_X) ? "mouse:X" : "mouse:Y"; break;
    case JB_TGT_MOUSE_BTN:  t = (b->target_index == JB_MOUSE_SELECT) ? "mouse:S" :
                                (b->target_index == JB_MOUSE_MENU)   ? "mouse:M" : "mouse:A"; break;
    case JB_TGT_KEY:        sprintf (detail, "key:%d", b->target_index);    t = detail; break;
    case JB_TGT_ADC:        sprintf (detail, "adc:%d", b->target_index);    t = detail; break;
    case JB_TGT_ADC_BTN:    sprintf (detail, "adcbtn:%d", b->target_index); t = detail; break;
  }

  if (b->flags & JB_FLAG_INVERT) sprintf (buf, "%s (inv)", t);
  else                           strcpy (buf, t);
}

/* The targets the Add StringSet offers. `label` is what it shows and what the
   handler parses back with parse_add_label. A concrete SINGULAR target (one
   source only) is offered just when nothing already drives it; shareable and
   the numeric "..." stand-ins are always offered. Picking a "..." opens
   BindEdit for the number; picking a concrete one pre-fills it. */
struct add_target { const char *label; int kind; int index; BOOL singular; BOOL numeric; };
static const struct add_target add_targets [] = {
  { "stick:8bitX",  JB_TGT_SLOT_AXIS,  JB_AXIS_8X,      TRUE,  FALSE },
  { "stick:8bitY",  JB_TGT_SLOT_AXIS,  JB_AXIS_8Y,      TRUE,  FALSE },
  { "stick:16bitX", JB_TGT_SLOT_AXIS,  JB_AXIS_16X,     TRUE,  FALSE },
  { "stick:16bitY", JB_TGT_SLOT_AXIS,  JB_AXIS_16Y,     TRUE,  FALSE },
  { "mouse:X",      JB_TGT_MOUSE_MOVE, JB_MOUSE_X,      TRUE,  FALSE },
  { "mouse:Y",      JB_TGT_MOUSE_MOVE, JB_MOUSE_Y,      TRUE,  FALSE },
  { "mouse:S",      JB_TGT_MOUSE_BTN,  JB_MOUSE_SELECT, FALSE, FALSE },
  { "mouse:M",      JB_TGT_MOUSE_BTN,  JB_MOUSE_MENU,   FALSE, FALSE },
  { "mouse:A",      JB_TGT_MOUSE_BTN,  JB_MOUSE_ADJUST, FALSE, FALSE },
  { "stickbtn:...", JB_TGT_SLOT_BTN,   0,               FALSE, TRUE  },
  { "key:...",      JB_TGT_KEY,        0,               FALSE, TRUE  },
  { "adc:...",      JB_TGT_ADC,        0,               TRUE,  TRUE  },
  { "adcbtn:...",   JB_TGT_ADC_BTN,    0,               FALSE, TRUE  }
};
#define NUM_ADD_TARGETS	((int) (sizeof add_targets / sizeof add_targets [0]))

/******	target_is_mapped() / build_add_list() / parse_add_label() *\

Purpose:	Helpers for the per-axis Add StringSet. target_is_mapped tests
		whether any binding already drives a given target; build_add_list
		builds the "Add,<target>,..." choice string (a leading "Add"
		placeholder + the offerable targets); parse_add_label maps a
		picked label back to its add_target.

\**************************************************************************/

static BOOL target_is_mapped (const joy_binding *t, int n, int kind, int index)
{
  int i;
  for (i = 0; i < n; i++)
    if (t [i].target_kind == kind && (int) t [i].target_index == index) return TRUE;
  return FALSE;
}

static void build_add_list (const joy_binding *table, int count, BOOL digital_only, char *out)
{
  int i, first = 1;
  out [0] = 0;
  for (i = 0; i < NUM_ADD_TARGETS; i++)
  {
    const struct add_target *a = &add_targets [i];
    /* A button (digital) source can only drive digital targets - the analogue
       kinds (stick axis, mouse move, ADC channel) are not offered for it. The
       canonical rule lives in the module (binding_validate rejects a
       button->analogue write); this mirror just keeps them out of the UI. */
    if (digital_only && (a->kind == JB_TGT_SLOT_AXIS || a->kind == JB_TGT_MOUSE_MOVE ||
                         a->kind == JB_TGT_ADC))
      continue;
    /* A concrete singular target already in use can't take another source. */
    if (a->singular && !a->numeric && target_is_mapped (table, count, a->kind, a->index))
      continue;
    if (!first) strcat (out, ",");
    strcat (out, a->label);
    first = 0;
  }
}

static const struct add_target *parse_add_label (const char *s)
{
  int i;
  for (i = 0; i < NUM_ADD_TARGETS; i++)
    if (strcmp (s, add_targets [i].label) == 0) return &add_targets [i];
  return NULL;
}

/******	refresh_axis_rows() ***************************************\

Purpose:	Fill one source's summary + Edit/Add pickers (an axis row, or
		the single button row) from the binding table: the summary text,
		the Edit StringSet (this source's current bindings, to pick one
		to edit) and the Add StringSet (targets that can be added - only
		digital ones for a button source). Pickers act as menus, so their
		shown value is reset to "" (blank at rest, any pick fires); one
		whose menu would be empty is faded so it can't open blank.

\**************************************************************************/

static void fill_source_pickers (ObjectId win, const joy_binding *table, int count,
                                 int src_type, int src_index,
                                 ComponentId summary_c, ComponentId edit_c, ComponentId add_c)
{
  char summary [256], editlist [256], addlist [256];
  int slen = 0, elen = 0, j;

  summary [0] = 0;
  editlist [0] = 0;
  for (j = 0; j < count; j++)
  {
    const joy_binding *b = &table [j];
    char one [40];
    if (b->source_type != src_type || (int) b->source_index != src_index) continue;
    format_target (b, one);
    if (slen) slen += sprintf (summary + slen, ", ");
    slen += sprintf (summary + slen, "%s", one);
    if (elen) elen += sprintf (editlist + elen, ",");
    elen += sprintf (editlist + elen, "%s", one);
  }
  build_add_list (table, count, (BOOL) (src_type == JB_SRC_BUTTON), addlist);

  displayfield_set_value  (0, win, summary_c, slen ? summary : UNMAPPED_TEXT);
  stringset_set_available (0, win, edit_c, editlist);
  stringset_set_selected  (0, win, edit_c, "");
  stringset_set_available (0, win, add_c, addlist);
  stringset_set_selected  (0, win, add_c, "");
  fade_gadget (win, edit_c, elen == 0);
  fade_gadget (win, add_c, addlist [0] == 0);
}

/******	refresh_axis_rows() / refresh_button_row() ***************\

Purpose:	Refresh the axis rows, or the single button row, from a binding
		table already in memory - the owning window's DRAFT, never a
		fresh live read (see struct detail_win). Called after setup and
		after any edit commits. The button row shows one button at a
		time (whichever the button-number picker has selected).

\**************************************************************************/

static void refresh_axis_rows (ObjectId win, const joy_binding *table, int count, int num_axes)
{
  int i;

  for (i = 0; i < num_axes && i < AXIS_MAX_DISPLAY; i++)
    fill_source_pickers (win, table, count, JB_SRC_AXIS, i,
                         devdetail_axis_summary (i), devdetail_axis_edit (i), devdetail_axis_add (i));
}

static void refresh_button_row (ObjectId win, const joy_binding *table, int count, int button)
{
  if (button < 0) return;

  fill_source_pickers (win, table, count, JB_SRC_BUTTON, button,
                       devdetail_btnsummary, devdetail_btnedit, devdetail_btnadd);
}

/* The button number currently chosen in the button-bindings row, or -1. */
static int selected_button (ObjectId win)
{
  char buf [16];
  int used = 0;
  if (stringset_get_selected (0, win, devdetail_btnpick, buf, sizeof buf, &used) != NULL) return -1;
  return buf [0] ? atoi (buf) : -1;
}

/******	clone_axis_rows() *****************************************\

Purpose:	Clones DevDetail's template axis row (row 0: label, value bar,
		summary, "..." button) down to `count` rows, so ResEd only holds
		one. Each row n gets component ids 0x200 + n*0x10 + sub and is
		shifted down by n row-pitches (the label gadget's height plus a
		gap). Row 0 already exists; we add 1..count-1.

\**************************************************************************/

#define AXIS_ROW_GAP	8	/* OS units between cloned axis rows */

static int clone_axis_rows (ObjectId win, const char *tmpl, int count)
{
  void *tp, *desc;
  int size, sub, n, pitch;
  char buf [256];
  BBox lbox;

  if (toolbox_template_lookup (0, (char *) tmpl, (void *) &tp) != NULL) return 0;

  /* row pitch = the template label's height + a gap. Computed even when there
     is nothing to clone, since the caller needs it to collapse the reserved
     axis band down to `count` rows. */
  if (window_extract_gadget_info (0, (ObjectTemplateHeader *) tp, devdetail_axis_label (0), &desc, &size) != NULL) return 0;
  lbox = *(BBox *) ((char *) desc + GADGET_BOX_OFF);
  pitch = (lbox.ymax - lbox.ymin) + AXIS_ROW_GAP;

  if (count <= 1) return pitch;

  for (sub = 0; sub < 5; sub++)		/* label, range, summary, edit, add */
  {
    ComponentId base = devdetail_axis_label (0) + sub;
    BBox box0;
    if (window_extract_gadget_info (0, (ObjectTemplateHeader *) tp, base, &desc, &size) != NULL) continue;
    if (size <= 0 || size > (int) sizeof buf) continue;
    memcpy (buf, desc, size);
    box0 = *(BBox *) (buf + GADGET_BOX_OFF);
    for (n = 1; n < count; n++)
    {
      BBox *b = (BBox *) (buf + GADGET_BOX_OFF);
      b->ymax = box0.ymax - n * pitch;
      b->ymin = box0.ymin - n * pitch;
      *(ComponentId *) (buf + GADGET_COMPID_OFF) = devdetail_axis_label (n) + sub;
      window_add_gadget (0, win, (Gadget *) buf, NULL);
    }
  }
  return pitch;
}

/******	move_gadget_by() ******************************************\

Purpose:	Slide one gadget vertically by `dy` OS units (positive = up).
			Missing gadgets are skipped silently, so callers can pass a
			whole grid's worth of ids without pre-checking which exist.

\**************************************************************************/

static void move_gadget_by (ObjectId win, ComponentId comp, int dy)
{
  BBox box;
  if (gadget_get_bbox (0, win, comp, &box) != NULL) return;
  box.ymin += dy;
  box.ymax += dy;
  gadget_move_gadget (0, win, comp, &box);
}

/******	collapse_axis_area() **************************************\

Purpose:	Normalise the vertical layout to the real axis count, the SAME
			way regardless of how many axes there are (so 6 and 16 look
			consistent). Both gaps are anchored to the last populated row:
			  - the Axes box bottom is set one BOTTOM_MARGIN below it;
			  - the Buttons box (its grid and the button-bindings row) is
			    moved so its top sits one BUTTONS_GAP below that.
			The work-area bottom follows by the same delta, and the caller
			resizes the visible window to match. The footer (Default/Cancel/
			Set) is a bottom tool-bar pane the Toolbox keeps pinned to the
			window bottom, so it is not touched here.

\**************************************************************************/

/* Both anchored to the last axis row, so they map 1:1 to what you see and are
   identical for any axis count:
     BOTTOM_MARGIN - last axis row -> Axes box bottom edge
     BUTTONS_GAP   - Axes box bottom edge -> Buttons box top edge */
#define AXIS_BOX_BOTTOM_MARGIN	32	/* OS units */
#define AXIS_TO_BUTTONS_GAP	33	/* OS units */

static int collapse_axis_area (ObjectId win, int shown, int pitch)
{
  BBox last, box, btns;
  int axes_bottom, delta, i;

  (void) pitch;	/* geometry is anchored to real bboxes, not pitch */

  if (shown < 1) return 0;

  /* Live positions of the last row and the two boxes - so nothing depends on
     the ResEd row spacing matching our clone pitch. */
  if (gadget_get_bbox (0, win, devdetail_axis_label (shown - 1), &last) != NULL) return 0;
  if (gadget_get_bbox (0, win, devdetail_axes_box, &box)  != NULL) return 0;
  if (gadget_get_bbox (0, win, devdetail_buttons_box, &btns) != NULL) return 0;

  /* Anchor the Axes box bottom one margin below the last row (top stays put). */
  axes_bottom = last.ymin - AXIS_BOX_BOTTOM_MARGIN;
  box.ymin = axes_bottom;
  gadget_move_gadget (0, win, devdetail_axes_box, &box);

  /* Move the whole lower assembly (Buttons box + grid) so its top sits one gap
     below that. delta is the shift (Y grows upward: + = up, - = down). */
  delta = (axes_bottom - AXIS_TO_BUTTONS_GAP) - btns.ymax;

  move_gadget_by (win, devdetail_buttons_box, delta);
  for (i = 0; i < BUTTON_MAX_DISPLAY; i++)
  {
    move_gadget_by (win, devdetail_btn (i), delta);
    move_gadget_by (win, devdetail_btnlabel (i), delta);
  }
  /* The button-bindings row lives with the button block, so it moves too. */
  move_gadget_by (win, devdetail_btnpick, delta);
  move_gadget_by (win, devdetail_btnsummary, delta);
  move_gadget_by (win, devdetail_btnedit, delta);
  move_gadget_by (win, devdetail_btnadd, delta);

  /* Track the work-area bottom by the same delta; the caller resizes the
     visible window to match (the extent alone does not resize it). */
  if (window_get_extent (0, win, &box) == NULL)
  {
    box.ymin += delta;
    window_set_extent (0, win, &box);
  }
  return delta;
}

/******	setup_device_detail() *************************************\

Purpose:	One-time setup of a freshly-created DevDetail window for
		slot 'slot' - populates the fixed StringSet choice lists and
		button captions (each window is its own object, so these are
		per-window, not global), then fills in this device's identity,
		current mapping, axis shape and button count. Also snapshots
		the live mapping into `d`'s draft (see struct detail_win) -
		the starting point for editing.

\**************************************************************************/

static int setup_device_detail (ObjectId win, int slot, struct detail_win *d)
{
  int devinfo_ptr, axiscount, axisinfo_ptr, axis_record_size;
  unsigned int i, num_buttons = 0, num_axes = 0, shown = 0;
  int axis_pitch = 0;
  char title [256];

  /* "Mapped to" choices are fixed (not device-dependent). Range is 0..
     STICK_MAX-1, matching the legacy targets Joystick_MapStick accepts. */
  {
    char choices [128] = UNMAPPED_TEXT;
    char one [16];
    int n;
    for (n = 0; n < STICK_MAX; n++)
    {
      sprintf (one, ",Stick %d", n);
      strcat (choices, one);
    }
    stringset_set_available (0, win, devdetail_stick, choices);
    /* Baseline so the field is never blank - the mapping read below overrides
       it with "Stick N" when one is assigned. */
    stringset_set_selected (0, win, devdetail_stick, UNMAPPED_TEXT);
  }

  /* Only btn(0)/btnlabel(0) are laid out in ResEd; clone the rest into a grid
     (must happen before the button fade/state loop below). The buttons show
     btnon/btnoff sprites (no text caption); the labels show the index. */
  clone_grid (win, "DevDetail", devdetail_btn (0),      BUTTON_MAX_DISPLAY, BUTTON_GRID_COLS, devdetail_btn (0), FALSE, DEV_BUTTON_COL_GAP);
  clone_grid (win, "DevDetail", devdetail_btnlabel (0), BUTTON_MAX_DISPLAY, BUTTON_GRID_COLS, devdetail_btn (0), TRUE,  DEV_BUTTON_COL_GAP);

  /* Identity */
  if (have_deviceinfo && device_call (deviceinfo_swi, slot, NULL, &devinfo_ptr, NULL, NULL))
  {
    struct device_info *info = (struct device_info *) devinfo_ptr;
    char idbuf [32];

    displayfield_set_value (0, win, devdetail_manuf, info->manufacturer ? info->manufacturer : "");
    displayfield_set_value (0, win, devdetail_product, info->product ? info->product : "");
    displayfield_set_value (0, win, devdetail_serial, info->serial ? info->serial : "");
    sprintf (idbuf, "%04x:%04x", info->vendor_id, info->product_id);
    displayfield_set_value (0, win, devdetail_ids, idbuf);
    num_buttons = info->num_buttons;

    /* Title matches the master-list row text (see refresh_list) */
    {
      char *manuf = (info->manufacturer && info->manufacturer [0]) ? info->manufacturer : "";
      char *prod  = (info->product && info->product [0]) ? info->product : "(unnamed)";
      if (manuf [0])
        sprintf (title, "Device Slot %d: %s %s", slot, manuf, prod);
      else
        sprintf (title, "Device Slot %d: %s", slot, prod);
    }
  }
  else
    sprintf (title, "Device Slot %d", slot);
  window_set_title (0, win, title);

  /* Current legacy-stick mapping - seeds the draft (d->draft_stick); the
     StringSet is set from the draft, not the other way round, from here on. */
  d->draft_stick = JOY_NOT_MAPPED;
  if (have_getmapping)
  {
    int mapping_ptr;
    if (device_call (getmapping_swi, slot, NULL, &mapping_ptr, NULL, NULL))
      /* legacy_stick_number is the leading int32 of struct joystick_mapping */
      d->draft_stick = ((int *) mapping_ptr) [MAP_LEGACY_STICK];
  }
  {
    char buf [16];
    if (d->draft_stick == JOY_NOT_MAPPED) strcpy (buf, UNMAPPED_TEXT);
    else sprintf (buf, "Stick %d", d->draft_stick);
    stringset_set_selected (0, win, devdetail_stick, buf);
  }

  /* Snapshot the live binding table into the draft - all editing from here
     happens in RAM (see struct detail_win); Set is what actually writes it
     back out with Joystick_WriteBindings. */
  d->draft_count = read_bindings (slot, d->draft_bindings, JB_MAX_BINDINGS);
  if (d->draft_count < 0) d->draft_count = 0;

  /* Axis rows - clone one ResEd template row per real axis (capped at the
     display max), then fill each row's name and slider bounds. */
  if (have_axisinfo && device_call (axisinfo_swi, slot, &axis_record_size, &axiscount, &axisinfo_ptr, NULL))
  {
    num_axes = (unsigned int) axiscount;
    shown = (num_axes < AXIS_MAX_DISPLAY) ? num_axes : AXIS_MAX_DISPLAY;

    /* R1 is the size of one axis-info record specifically so callers don't
       assume a fixed struct layout - index with it, not sizeof(ours) */
    if (axis_record_size <= 0) axis_record_size = sizeof (struct axis_info);

    axis_pitch = clone_axis_rows (win, "DevDetail", (int) shown);

    for (i = 0; i < shown; i++)
    {
      struct axis_info *ax = (struct axis_info *) ((char *) axisinfo_ptr + i * axis_record_size);
      /* axis_label must be a DisplayField, not a Label - no runtime
         "set text" call exists for the Label class in this Toolbox */
      displayfield_set_value (0, win, devdetail_axis_label (i), ax->name ? ax->name : "");
      numberrange_set_bounds (7, win, devdetail_axis_range (i), ax->min, ax->max, 1, 0);
    }
  }

  /* Fill each axis row's summary + Edit/Add StringSets from the draft. */
  refresh_axis_rows (win, d->draft_bindings, d->draft_count, (int) num_axes);

  fade_gadget (win, devdetail_axis_overflow, num_axes <= AXIS_MAX_DISPLAY);
  if (num_axes > AXIS_MAX_DISPLAY)
  {
    char buf [64];
    sprintf (buf, "Device reports %u axes, showing first %d", num_axes, AXIS_MAX_DISPLAY);
    displayfield_set_value (0, win, devdetail_axis_overflow, buf);
  }
  else
    /* Wipe ResEd's design-time placeholder text - the field is faded, but
       the stale text still shows through */
    displayfield_set_value (0, win, devdetail_axis_overflow, "");

  /* Button indicators are read-only sprite Button gadgets (btnon/btnoff
     swapped via button_set_value in poll_device_detail). Start them all at
     "btnoff", then grey out (fade) the ones this device doesn't have - so
     present-but-idle (crisp btnoff) reads differently from absent (dimmed
     btnoff). The present ones get their live state from the poll. */
  for (i = 0; i < BUTTON_MAX_DISPLAY; i++)
  {
    button_set_value (0, win, devdetail_btn (i), "btnoff");
    fade_gadget (win, devdetail_btn (i), i >= num_buttons);
  }

  /* Button-bindings row: the which-button picker lists 0..num_buttons-1; start
     on button 0 and fill its row (faded whole if the device has no buttons). */
  {
    char blist [128];
    unsigned int b;
    int n = 0;
    blist [0] = 0;
    for (b = 0; b < num_buttons && b < BUTTON_MAX_DISPLAY; b++)
      n += sprintf (blist + n, b ? ",%u" : "%u", b);
    stringset_set_available (0, win, devdetail_btnpick, blist);
    stringset_set_selected  (0, win, devdetail_btnpick, "0");
    fade_gadget (win, devdetail_btnpick, num_buttons == 0);
    refresh_button_row (win, d->draft_bindings, d->draft_count, num_buttons ? 0 : -1);
  }

  /* (The Default button lives in the footer tool-bar pane, which isn't
     realised until the window is shown - its fade is applied in open_detail,
     after the show.) */

  /* Now every gadget is in place, reclaim the unused axis rows so the window
     opens sized to this device rather than to AXIS_MAX_DISPLAY. Returns how
     much shorter the window should open (0 = full height). */
  return collapse_axis_area (win, (int) shown, axis_pitch);
}

/******	refresh_device_detail() **********************************\

Purpose:	Re-render a DevDetail window's editable rows - the legacy-stick
		picker and each axis/button row's binding summary - from `d`'s
		DRAFT, without touching its geometry. Used after a BindEdit
		commit/delete and after Default resets the draft, neither of
		which changes the device's shape - so the cloned rows, button
		grid and window size that setup_device_detail already built
		stay put (re-running the full setup would clone and collapse a
		second time).

\**************************************************************************/

static void refresh_device_detail (struct detail_win *d)
{
  ObjectId win = d->obj;
  int axiscount, axisinfo_ptr, axis_record_size;
  int num_axes = 0;
  char buf [16];

  if (d->draft_stick == JOY_NOT_MAPPED) strcpy (buf, UNMAPPED_TEXT);
  else sprintf (buf, "Stick %d", d->draft_stick);
  stringset_set_selected (0, win, devdetail_stick, buf);

  if (have_axisinfo && device_call (axisinfo_swi, d->index, &axis_record_size, &axiscount, &axisinfo_ptr, NULL))
    num_axes = axiscount;

  refresh_axis_rows (win, d->draft_bindings, d->draft_count, num_axes);
  refresh_button_row (win, d->draft_bindings, d->draft_count, selected_button (win));	/* keep the shown button's row in step */
}

/******	poll_device_detail() **************************************\

Purpose:	Refreshes one open DevDetail's live axis slider values and
		button indicators from Joystick_AxisValues.

\**************************************************************************/

static void poll_device_detail (struct detail_win *d)
{
  ObjectId win = d->obj;
  int slot = d->index;
  int count, values_ptr, buttons_bitmask;
  unsigned int i;

  if (have_axisvalues && device_call (axisvalues_swi, slot, NULL, &count, &values_ptr, &buttons_bitmask))
  {
    int *values = (int *) values_ptr;
    unsigned int bm = (unsigned int) buttons_bitmask;
    for (i = 0; i < AXIS_MAX_DISPLAY && i < (unsigned int) count; i++)
      numberrange_set_value (0, win, devdetail_axis_range (i), values [i]);
    /* Only re-plot buttons whose state changed since last poll */
    for (i = 0; i < BUTTON_MAX_DISPLAY; i++)
      if (((bm ^ d->last_buttons) >> i) & 1)
        button_set_value (0, win, devdetail_btn (i), ((bm >> i) & 1) ? "btnon" : "btnoff");
    d->last_buttons = bm;
  }
}

/*========================================================================*/
/* Opening / closing detail windows					  */
/*========================================================================*/

/******	screen_height_os() ***************************************\

Purpose:	Current screen height in OS units (0 if it can't be read).

\**************************************************************************/

static int screen_height_os (void)
{
  _kernel_swi_regs r;
  int ywind, yeig;

  r.r[0] = -1; r.r[1] = 12;			/* YWindLimit (pixels - 1) */
  if (_kernel_swi (OS_ReadModeVariable, &r, &r) != NULL) return 0;
  ywind = r.r[2];
  r.r[0] = -1; r.r[1] = 4;			/* YEigFactor */
  if (_kernel_swi (OS_ReadModeVariable, &r, &r) != NULL) return 0;
  yeig = r.r[2];
  return (ywind + 1) << yeig;
}

/******	scroll_to_top() *******************************************\

Purpose:	Re-opens a just-shown window scrolled to the top of its work
		area, optionally `shrink` OS units shorter. DevDetail is laid
		out tall (for AXIS_MAX_DISPLAY axes) and toolbox_show_object
		opens it at the template's stored size and scroll offset;
		force it to the top and, when setup collapsed unused axis rows,
		raise the visible bottom edge to match so it opens at its
		natural height rather than over-tall with empty space.

		If the fitted window is still taller than the screen (a
		many-axis device on a small mode), clamp the visible height to
		the screen so the whole window - including its footer - stays
		on-screen; the template's vertical scrollbar then reaches the
		rest. On any normal desktop the window fits and this is a no-op.

\**************************************************************************/

/* Clearances (OS units) kept below/above the work area when fitting a window
   to the screen: room for the icon bar at the bottom and the title bar at the
   top, so a clamped or nudged window is fully usable, not exactly screen-tall. */
#define WINDOW_BOTTOM_CLEAR	((int) 128)
#define WINDOW_TITLE_CLEAR	((int) 48)

static void scroll_to_top (ObjectId obj, int shrink)
{
  WimpGetWindowStateBlock state;
  WindowShowObjectBlock spec;
  BBox ext;
  int scr, avail, want_h;

  (void) shrink;	/* height now comes from the work-area extent, not a delta */

  if (window_get_wimp_handle (0, obj, &state.window_handle) != NULL) return;
  if (wimp_get_window_state (&state) != NULL) return;
  if (window_get_extent (0, obj, &ext) != NULL) return;

  /* Open as tall as the content actually needs - the work-area extent, which
     setup/collapse has already sized to the real content - keeping the top
     where the pointer-placed show put it. */
  want_h = ext.ymax - ext.ymin;

  scr = screen_height_os ();

  /* Clamp the visible HEIGHT to the screen (scrollbar reaches the rest). */
  avail = scr - WINDOW_BOTTOM_CLEAR - WINDOW_TITLE_CLEAR;
  if (avail > 0 && want_h > avail) want_h = avail;

  state.visible_area.ymin = state.visible_area.ymax - want_h;

  /* Then slide the whole window on-screen so the footer stays visible: up if
     the bottom fell off, then down if the top did. */
  if (scr > 0)
  {
    if (state.visible_area.ymin < WINDOW_BOTTOM_CLEAR)
    {
      int d = WINDOW_BOTTOM_CLEAR - state.visible_area.ymin;
      state.visible_area.ymin += d; state.visible_area.ymax += d;
    }
    if (state.visible_area.ymax > scr - WINDOW_TITLE_CLEAR)
    {
      int d = state.visible_area.ymax - (scr - WINDOW_TITLE_CLEAR);
      state.visible_area.ymin -= d; state.visible_area.ymax -= d;
    }
  }

  /* Re-show at the computed size THROUGH the Toolbox (FullSpec), not via a
     direct Wimp_OpenWindow: the Window module only repositions attached tool
     bars (our footer pane) when it opens the window itself, so a bare
     wimp_open_window would leave the footer floating until the next manual
     resize. */
  spec.visible_area = state.visible_area;
  spec.xscroll = 0;
  spec.yscroll = 0;
  spec.behind  = -1;				/* open on top */
  toolbox_show_object (0, obj, Toolbox_ShowObject_FullSpec, &spec, NULL_ObjectId, NULL_ComponentId);
}

/******	open_detail() *********************************************\

Purpose:	Opens (or re-fronts, if already open) the detail window for
		one stick/slot. Creates the object on demand, tracks it, and
		shows it at the pointer. Cleanup happens later in
		toolboxe_windowhidden.

\**************************************************************************/

static void open_detail (int kind, int index)
{
  struct detail_win *d;
  ObjectId obj;

  /* Already open? Bring it to the front IN PLACE - don't re-show at the
     pointer, which just makes the existing window jump. One window per
     stick/slot. */
  d = find_detail (kind, index);
  if (d != NULL)
  {
    WimpGetWindowStateBlock st;
    if (window_get_wimp_handle (0, d->obj, &st.window_handle) == NULL &&
        wimp_get_window_state (&st) == NULL)
    {
      st.behind = -1;			/* -1 = top of the window stack */
      wimp_open_window ((WimpOpenWindowBlock *) &st);
    }
    return;
  }

  d = find_free_detail ();
  if (d == NULL) return;	/* can't happen - array sized to the max */

  if (toolbox_create_object (0, (void *) (kind == KIND_STICK ? "StickDetail" : "DevDetail"), &obj) != NULL)
    return;

  d->obj = obj;
  d->kind = kind;
  d->index = index;
  d->last_buttons = 0;		/* matches setup's all-"btnoff" initial state */

  {
    int shrink = 0;
    if (kind == KIND_STICK) setup_stick_detail (obj, index);
    else                    shrink = setup_device_detail (obj, index, d);

    toolbox_show_object (0, obj, Toolbox_ShowObject_AtPointer, NULL, NULL_ObjectId, NULL_ComponentId);
    scroll_to_top (obj, shrink);	/* top of work area, sized to the device */

    /* Fade Default (Joystick_ReadDefaultBindings) if that SWI isn't present.
       It's in the footer tool-bar pane, realised only now the window is shown. */
    if (kind == KIND_DEVICE)
    {
      ObjectId foot = footer_pane (obj);
      if (foot != NULL_ObjectId) fade_gadget (foot, devdetail_default, !have_readdefaultbindings);
    }
  }
}

/******	toolboxe_windowhidden() ***********************************\

Purpose:	Handles Window_HasBeenHidden - fires whenever a detail window
		is hidden by ANY route (its Close/Set/Cancel button, the
		title-bar close icon, or our own toolbox_hide_object on
		unplug). Deletes the object and frees its tracker slot.

		Needs "Generate HasBeenHidden event" ticked in ResEd on the
		master AND both detail templates, or this never fires - the
		event is gated on that flag (see c/hide in the Window module).
		On the master it means "closing the list window quits the app"
		(so the title-bar close icon behaves like the Close button);
		on a detail window it means "delete the object and free its
		tracker slot".

\**************************************************************************/

static int toolboxe_windowhidden (int event_code, ToolboxEvent *event, IdBlock *id_block, void *handle)
{
  struct detail_win *d;

  if (id_block->self_id == mainwindow_id)
  {
    quit = TRUE;
    return 1;
  }

  d = find_detail_by_obj (id_block->self_id);
  if (d != NULL && d->obj != NULL_ObjectId)
  {
    toolbox_delete_object (0, d->obj);
    d->obj = NULL_ObjectId;
  }
  return 1;
}

/*========================================================================*/
/* Master list								  */
/*========================================================================*/

/******	rescan_swis() *********************************************\

Purpose:	(Re-)probes the Joystick SWIs. Called once at startup and then
		periodically, so a provider module loaded (or killed) after the
		app started is noticed. If either list-driving tier (Read =
		sticks, Enumerate = devices) appeared or disappeared, forces a
		list rebuild.

		A failed probe leaves the SWI number at 0 (= OS_WriteC); the
		have_* guard on every call site is what stops us calling the
		wrong SWI with garbage - so always capture the result.

\**************************************************************************/

static void rescan_swis (void)
{
  BOOL was_read = have_stick_read, was_enum = have_stick_enumerate;

  have_stick_read      = probe_swi ("Joystick_Read", &stick_read_swi);
  have_stick_enumerate = probe_swi ("Joystick_Enumerate", &enumerate_swi);
  have_deviceinfo      = probe_swi ("Joystick_DeviceInfo", &deviceinfo_swi);
  have_axisinfo        = probe_swi ("Joystick_AxisInfo", &axisinfo_swi);
  have_axisvalues      = probe_swi ("Joystick_AxisValues", &axisvalues_swi);
  have_getmapping      = probe_swi ("Joystick_GetMapping", &getmapping_swi);
  have_setstick        = probe_swi ("Joystick_SetStick", &setstick_swi);
  have_readbindings    = probe_swi ("Joystick_ReadBindings", &readbindings_swi);
  have_writebindings   = probe_swi ("Joystick_WriteBindings", &writebindings_swi);
  have_readdefaultbindings = probe_swi ("Joystick_ReadDefaultBindings", &readdefaultbindings_swi);
  have_save            = probe_swi ("Joystick_SaveMapping", &save_swi);
  have_emulate         = probe_swi ("Joystick_Emulate", &emulate_swi);
  have_control         = probe_swi ("Joystick_Control", &control_swi);
  have_savesettings    = probe_swi ("Joystick_SaveSettings", &savesettings_swi);

  /* A whole tier came or went -> the list's contents change, so rebuild it
     (refresh_list is otherwise gated on the enumerate generation counter,
     which doesn't move when a module is simply loaded/unloaded) */
  if (have_stick_read != was_read || have_stick_enumerate != was_enum)
    list_built = FALSE;
}

/******	query_flag() **********************************************\

Purpose:	Read one Emulate/Control flag's current state (R1=-1, the
		query convention every Joystick_Emulate/Joystick_Control caller
		uses). Returns 0 if the SWI errors - callers only use this to
		seed a checkbox, so a failure just leaves it unticked rather
		than reporting an error nobody asked for.

\**************************************************************************/

static int query_flag (int swi, int which)
{
  _kernel_swi_regs regs;
  regs.r[0] = which;
  regs.r[1] = -1;
  if (_kernel_swi (swi, &regs, &regs) != NULL) return 0;
  return regs.r[0] ? 1 : 0;
}

/* Apply one Emulate/Control flag live. */
static void apply_flag (int swi, int which, int state)
{
  _kernel_swi_regs regs;
  regs.r[0] = which;
  regs.r[1] = state ? 1 : 0;
  _kernel_swi (swi, &regs, &regs);
}

/******	setup_global_settings() ***********************************\

Purpose:	One-time setup of the master window's Emulation/Control
		checkboxes (see joylist_set in h/Ids): seeds each from the
		live module state, and fades a whole group - or Set itself -
		when its SWI isn't present. Called once at startup; unlike the
		live-refreshing parts of the master list, these are never
		re-synced from live state afterwards, the same "seed once,
		then it's the user's to edit until Set" principle a DevDetail
		draft already follows.

\**************************************************************************/

static void setup_global_settings (void)
{
  fade_gadget (mainwindow_id, joylist_emu_serialport, !have_emulate);
  fade_gadget (mainwindow_id, joylist_emu_joy,         !have_emulate);
  fade_gadget (mainwindow_id, joylist_emu_adc,         !have_emulate);
  fade_gadget (mainwindow_id, joylist_ctrl_mouse,      !have_control);
  fade_gadget (mainwindow_id, joylist_ctrl_keyboard,   !have_control);
  fade_gadget (mainwindow_id, joylist_set,             !have_savesettings);

  if (have_emulate)
  {
    optionbutton_set_state (0, mainwindow_id, joylist_emu_serialport,
                            query_flag (emulate_swi, JOYSTICK_EMULATE_SERIALPORT));
    optionbutton_set_state (0, mainwindow_id, joylist_emu_joy,
                            query_flag (emulate_swi, JOYSTICK_EMULATE_JOY));
    optionbutton_set_state (0, mainwindow_id, joylist_emu_adc,
                            query_flag (emulate_swi, JOYSTICK_EMULATE_ADC));
  }
  if (have_control)
  {
    optionbutton_set_state (0, mainwindow_id, joylist_ctrl_mouse,
                            query_flag (control_swi, JOYSTICK_CONTROL_MOUSE));
    optionbutton_set_state (0, mainwindow_id, joylist_ctrl_keyboard,
                            query_flag (control_swi, JOYSTICK_CONTROL_KEYBOARD));
  }
}

/******	refresh_list() ********************************************\

Purpose:	Rebuilds the master ScrollList when the enumerated device
		set changes (or on first call). The STICK_MAX "Stick N" rows
		are static; device rows track Joystick_Enumerate. Also closes
		any open device-detail window whose slot has just gone, and
		re-selects the previously-selected row if it survived the
		rebuild. Cheap to call every poll - the generation check
		makes it a no-op unless something actually changed.

\**************************************************************************/

static void refresh_list (void)
{
  _kernel_swi_regs regs;
  unsigned int bitmask = 0, generation = 0;
  int i, j, slot, found;

  if (have_stick_enumerate)
  {
    if (_kernel_swi (enumerate_swi, &regs, &regs) != NULL) return;
    bitmask = regs.r[0];
    generation = regs.r[1];
  }

  /* Rebuild when the device set changed (generation), OR when list_built was
     forced FALSE by rescan_swis() because a provider module came or went. */
  if (list_built && (!have_stick_enumerate || generation == enum_generation))
    return;
  enum_generation = generation;
  list_built = TRUE;

  if (row_count > 0)
    scrolllist_delete_items (0, mainwindow_id, joylist, 0, row_count - 1);
  row_count = 0;

  if (have_stick_read)
    for (i = 0; i < STICK_MAX; i++)
    {
      char buf [24];
      sprintf (buf, "Stick %d", i);
      /* NULL/NULL for sprite area+name, not 0/"" - these rows are text-only.
         "" is a non-NULL empty sprite name, which makes the ScrollList treat
         every row as HAVING a sprite and run its sprite/mode-plot path per
         row (see _get_size_of_sprites / redraw in scrolllist,fff, gated on
         "if (sprite_name)"); NULL skips all of that */
      scrolllist_add_item (0, mainwindow_id, joylist, buf, NULL, NULL, -1);
      row_map [row_count].kind = KIND_STICK;
      row_map [row_count].index = i;
      row_count++;
    }

  if (have_stick_enumerate)
    for (slot = 0; slot < DEVICE_SLOT_MAX; slot++)
    {
      int devinfo_ptr;
      char buf [256];

      if (!(bitmask & (1u << slot))) continue;
      if (!have_deviceinfo || !device_call (deviceinfo_swi, slot, NULL, &devinfo_ptr, NULL, NULL)) continue;

      {
        struct device_info *info = (struct device_info *) devinfo_ptr;
        char *manuf = (info->manufacturer && info->manufacturer [0]) ? info->manufacturer : "";
        char *prod  = (info->product && info->product [0]) ? info->product : "(unnamed)";
        /* Plenty of list width now - spell it out. buf is generous (256) so
           even a device with long USB manufacturer+product descriptors can't
           overrun it. Skip the manufacturer if it's blank, to avoid a gap. */
        if (manuf [0])
          sprintf (buf, "Device Slot %d: %s %s", slot, manuf, prod);
        else
          sprintf (buf, "Device Slot %d: %s", slot, prod);
      }
      /* NULL/NULL - text-only row, see the stick-row call above */
      scrolllist_add_item (0, mainwindow_id, joylist, buf, NULL, NULL, -1);
      row_map [row_count].kind = KIND_DEVICE;
      row_map [row_count].index = slot;
      row_count++;
    }

  /* Preserve the selection across the rebuild if its row still exists */
  found = -1;
  if (sel_kind >= 0)
    for (i = 0; i < row_count; i++)
      if (row_map [i].kind == sel_kind && row_map [i].index == sel_index) { found = i; break; }

  if (found >= 0)
    scrolllist_select_item (0, mainwindow_id, joylist, found);
  else
    sel_kind = -1;

  fade_gadget (mainwindow_id, joylist_edit, sel_kind < 0);

  /* Close any open detail window whose item is no longer in the list - a
     device was unplugged, or its whole tier's provider module went away.
     toolbox_hide_object -> Window_HasBeenHidden -> delete + free slot. This
     one check covers both cases (device hot-unplug and module unload). */
  for (i = 0; i < MAX_DETAIL; i++)
    if (detail [i].obj != NULL_ObjectId)
    {
      BOOL listed = FALSE;
      for (j = 0; j < row_count; j++)
        if (row_map [j].kind == detail [i].kind && row_map [j].index == detail [i].index)
          { listed = TRUE; break; }
      if (!listed) toolbox_hide_object (0, detail [i].obj);
    }
}

/*========================================================================*/
/* BindEdit dialogue (edit or add ONE binding)				  */
/*========================================================================*/

/* Target kinds offered, in the StringSet's order. kind_names[i] is what the
   StringSet shows and returns; kind_values[i] is the matching JB_TGT_*. */
static const char *const kind_choices =
  "Stick axis,Stick button,Mouse move,Mouse button,Key,ADC channel,ADC fire";
/* The digital subset, offered when the source is a button (a button can't
   drive an analogue target - see binding_validate in the module). */
static const char *const kind_choices_digital =
  "Stick button,Mouse button,Key,ADC fire";
static const char *const kind_names [7] =
  { "Stick axis", "Stick button", "Mouse move", "Mouse button", "Key", "ADC channel", "ADC fire" };
static const int kind_values [7] =
  { JB_TGT_SLOT_AXIS, JB_TGT_SLOT_BTN, JB_TGT_MOUSE_MOVE, JB_TGT_MOUSE_BTN,
    JB_TGT_KEY, JB_TGT_ADC, JB_TGT_ADC_BTN };

/******	bindedit_show_kind() **************************************\

Purpose:	Reconfigure the detail pickers for the chosen target kind.
		Kinds with a fixed set of sub-targets (stick axis, mouse move,
		mouse button) use the detail StringSet; the numeric kinds
		(stick button, key, ADC channel/fire) use the number field.
		The unused one is faded.

\**************************************************************************/

static void bindedit_show_kind (ObjectId win, int kind)
{
  const char *choices = NULL, *first = NULL;

  switch (kind)
  {
    case JB_TGT_SLOT_AXIS:  choices = "8bitX,8bitY,16bitX,16bitY"; first = "8bitX";  break;
    case JB_TGT_MOUSE_MOVE: choices = "X,Y";                       first = "X";      break;
    case JB_TGT_MOUSE_BTN:  choices = "Select,Menu,Adjust";        first = "Select"; break;
    default: break;	/* numeric detail */
  }

  if (choices)
  {
    stringset_set_available (0, win, bindedit_detail, (char *) choices);
    stringset_set_selected  (0, win, bindedit_detail, (char *) first);
  }
  /* Clear the number to a consistent blank whenever the kind changes, so a
     faded (unused) number field never shows a stale value from a previous
     numeric kind. bindedit_set_pickers fills the real value for numeric kinds
     straight after this. */
  writablefield_set_value (0, win, bindedit_number, "");

  fade_gadget (win, bindedit_detail, choices == NULL);
  fade_gadget (win, bindedit_number, choices != NULL);
}

/******	bindedit_set_pickers() ***********************************\

Purpose:	Drive the kind / detail / number / invert gadgets to represent
		one target (kind + index) and invert flag - used both to show an
		existing binding for editing and to pre-fill a chosen Add target.

\**************************************************************************/

static void bindedit_set_pickers (ObjectId win, int kind, int target_index, int invert)
{
  int i;

  for (i = 0; i < 7; i++)
    if (kind_values [i] == kind)
    { stringset_set_selected (0, win, bindedit_kind, (char *) kind_names [i]); break; }

  bindedit_show_kind (win, kind);		/* set detail choices + fade detail/number */

  switch (kind)
  {
    case JB_TGT_SLOT_AXIS:
    {
      const char *d = target_index == JB_AXIS_8X  ? "8bitX"  :
                      target_index == JB_AXIS_8Y  ? "8bitY"  :
                      target_index == JB_AXIS_16X ? "16bitX" : "16bitY";
      stringset_set_selected (0, win, bindedit_detail, (char *) d);
      break;
    }
    case JB_TGT_MOUSE_MOVE:
      stringset_set_selected (0, win, bindedit_detail, (char *) (target_index == JB_MOUSE_Y ? "Y" : "X"));
      break;
    case JB_TGT_MOUSE_BTN:
      stringset_set_selected (0, win, bindedit_detail,
        (char *) (target_index == JB_MOUSE_MENU   ? "Menu"   :
                  target_index == JB_MOUSE_ADJUST ? "Adjust" : "Select"));
      break;
    default:	/* numeric detail */
    {
      char n [16];
      sprintf (n, "%d", target_index);
      writablefield_set_value (0, win, bindedit_number, n);
      break;
    }
  }

  optionbutton_set_state (0, win, bindedit_invert, invert ? 1 : 0);
}

/******	bindedit_build() *****************************************\

Purpose:	Assemble the joy_binding described by the current picker state
		(source is the fixed bindedit_src_*). Returns FALSE if the
		selection is incomplete/invalid.

\**************************************************************************/

static BOOL bindedit_build (ObjectId win, joy_binding *b)
{
  char kbuf [24], dbuf [24], nbuf [16];
  int used = 0, kind = -1, ti = 0, invert = 0, i;

  if (stringset_get_selected (0, win, bindedit_kind, kbuf, sizeof kbuf, &used) != NULL) return FALSE;
  for (i = 0; i < 7; i++)
    if (strcmp (kbuf, kind_names [i]) == 0) { kind = kind_values [i]; break; }
  if (kind < 0) return FALSE;

  switch (kind)
  {
    case JB_TGT_SLOT_AXIS:
      if (stringset_get_selected (0, win, bindedit_detail, dbuf, sizeof dbuf, &used) != NULL) return FALSE;
      if      (!strcmp (dbuf, "8bitX"))  ti = JB_AXIS_8X;
      else if (!strcmp (dbuf, "8bitY"))  ti = JB_AXIS_8Y;
      else if (!strcmp (dbuf, "16bitX")) ti = JB_AXIS_16X;
      else if (!strcmp (dbuf, "16bitY")) ti = JB_AXIS_16Y;
      else return FALSE;
      break;
    case JB_TGT_MOUSE_MOVE:
      if (stringset_get_selected (0, win, bindedit_detail, dbuf, sizeof dbuf, &used) != NULL) return FALSE;
      ti = (!strcmp (dbuf, "Y")) ? JB_MOUSE_Y : JB_MOUSE_X;
      break;
    case JB_TGT_MOUSE_BTN:
      if (stringset_get_selected (0, win, bindedit_detail, dbuf, sizeof dbuf, &used) != NULL) return FALSE;
      ti = (!strcmp (dbuf, "Menu"))   ? JB_MOUSE_MENU :
           (!strcmp (dbuf, "Adjust")) ? JB_MOUSE_ADJUST : JB_MOUSE_SELECT;
      break;
    default:	/* numeric detail from the number field */
      nbuf [0] = 0;
      writablefield_get_value (0, win, bindedit_number, nbuf, sizeof nbuf, &used);
      ti = atoi (nbuf);
      if (ti < 0) return FALSE;
      break;
  }

  optionbutton_get_state (0, win, bindedit_invert, &invert);

  b->source_type  = (unsigned char) bindedit_src_type;
  b->source_index = (unsigned char) bindedit_src_index;
  b->target_kind  = (unsigned char) kind;
  b->flags        = (unsigned char) (invert ? JB_FLAG_INVERT : 0);
  b->target_index = (unsigned short) ti;
  b->reserved     = 0;
  b->param        = 0;
  return TRUE;
}

/******	bindedit_write() *****************************************\

Purpose:	Adopt a candidate table (built by OK or Delete) as BindEdit's
		working copy AND as the owning DevDetail's DRAFT (see struct
		detail_win), then re-render that window. No SWI call - BindEdit
		edits the in-RAM draft only; the module only ever sees it if/when
		the user later clicks the DevDetail footer's Set button, which is
		also where a bad combination (e.g. two sources both claiming the
		same singular target) is actually caught, by WriteBindings'
		validation.

\**************************************************************************/

static void bindedit_write (const joy_binding *tbl, int n)
{
  struct detail_win *d = find_detail (KIND_DEVICE, bindedit_slot);

  memcpy (bindedit_table, tbl, n * sizeof (joy_binding));
  bindedit_count = n;

  if (d == NULL) return;		/* owning window closed under us */

  memcpy (d->draft_bindings, tbl, n * sizeof (joy_binding));
  d->draft_count = n;
  refresh_device_detail (d);
}

/******	bindedit_commit() / bindedit_delete_binding() ************\

Purpose:	OK - build the edited/new binding into a copy of the table
		(replacing at edit_index, or appending when adding) and adopt it.
		Delete - drop the edited binding and adopt the result. Both just
		update the DRAFT (bindedit_write can't fail any more, since it no
		longer talks to the module); FALSE means the picker state itself
		was rejected (incomplete, or the table is already full).

\**************************************************************************/

static BOOL bindedit_commit (ObjectId win)
{
  joy_binding tmp [JB_MAX_BINDINGS], b;
  int n = bindedit_count;

  if (!bindedit_build (win, &b)) return FALSE;		/* incomplete selection */

  memcpy (tmp, bindedit_table, n * sizeof (joy_binding));
  if (bindedit_edit_index >= 0 && bindedit_edit_index < n)
    tmp [bindedit_edit_index] = b;			/* replace */
  else
  {
    if (n >= JB_MAX_BINDINGS) return FALSE;
    tmp [n++] = b;					/* append */
  }
  bindedit_write (tmp, n);
  return TRUE;
}

static BOOL bindedit_delete_binding (void)
{
  joy_binding tmp [JB_MAX_BINDINGS];
  int n = bindedit_count, i;

  if (bindedit_edit_index < 0 || bindedit_edit_index >= n) return FALSE;

  memcpy (tmp, bindedit_table, n * sizeof (joy_binding));
  for (i = bindedit_edit_index; i < n - 1; i++) tmp [i] = tmp [i + 1];
  n--;
  bindedit_write (tmp, n);
  return TRUE;
}

/******	bindedit_open_edit() / bindedit_open_add() ***************\

Purpose:	Open BindEdit on ONE binding. _edit loads an existing binding
		(Delete enabled); _add starts a new one pre-filled from a chosen
		target (Delete faded). Both snapshot the whole slot table so
		WriteBindings preserves the other bindings.

\**************************************************************************/

static BOOL bindedit_prepare (int slot, int src_type, int src_index)
{
  struct detail_win *d;

  if (bindedit_obj == NULL_ObjectId)
    if (toolbox_create_object (0, (void *) "BindEdit", &bindedit_obj) != NULL) return FALSE;

  bindedit_slot      = slot;
  bindedit_src_type  = src_type;
  bindedit_src_index = src_index;

  /* Snapshot from the owning DevDetail's DRAFT, not a fresh live read -
     BindEdit edits the same in-RAM table the footer's Set button later
     writes out (see struct detail_win). */
  d = find_detail (KIND_DEVICE, slot);
  bindedit_count = (d != NULL) ? d->draft_count : 0;
  if (d != NULL) memcpy (bindedit_table, d->draft_bindings, bindedit_count * sizeof (joy_binding));

  /* Offer only the digital target kinds for a button source. */
  stringset_set_available (0, bindedit_obj, bindedit_kind,
    (char *) (src_type == JB_SRC_BUTTON ? kind_choices_digital : kind_choices));
  return TRUE;
}

/* Set BindEdit's window title, e.g. "Edit binding: axis X" or "Add binding:
   button 3" - `verb` is "Edit" or "Add"; an axis name comes from the DevDetail
   row we were opened from, a button is named by its number. */
static void bindedit_set_title (ObjectId dev_win, const char *verb)
{
  char name [48], title [80];
  int used = 0;

  if (bindedit_src_type == JB_SRC_AXIS)
  {
    name [0] = 0;
    displayfield_get_value (0, dev_win, devdetail_axis_label (bindedit_src_index), name, sizeof name, &used);
    sprintf (title, "%s binding: axis %s", verb, name [0] ? name : "?");
  }
  else
    sprintf (title, "%s binding: button %d", verb, bindedit_src_index);

  window_set_title (0, bindedit_obj, title);
}

static void bindedit_open_edit (ObjectId dev_win, int slot, int src_type, int src_index, int table_index)
{
  const joy_binding *b;

  if (!bindedit_prepare (slot, src_type, src_index)) return;
  if (table_index < 0 || table_index >= bindedit_count) return;

  bindedit_edit_index = table_index;
  b = &bindedit_table [table_index];
  bindedit_set_pickers (bindedit_obj, b->target_kind, b->target_index,
                        (b->flags & JB_FLAG_INVERT) ? 1 : 0);
  fade_gadget (bindedit_obj, bindedit_delete, FALSE);		/* editing - Delete live */
  bindedit_set_title (dev_win, "Edit");

  toolbox_show_object (0, bindedit_obj, Toolbox_ShowObject_AtPointer, NULL, NULL_ObjectId, NULL_ComponentId);
}

static void bindedit_open_add (ObjectId dev_win, int slot, int src_type, int src_index, int kind, int target_index)
{
  if (!bindedit_prepare (slot, src_type, src_index)) return;

  bindedit_edit_index = -1;					/* adding */
  bindedit_set_pickers (bindedit_obj, kind, target_index, 0);
  fade_gadget (bindedit_obj, bindedit_delete, TRUE);		/* nothing to delete yet */
  bindedit_set_title (dev_win, "Add");

  toolbox_show_object (0, bindedit_obj, Toolbox_ShowObject_AtPointer, NULL, NULL_ObjectId, NULL_ComponentId);
}

/*========================================================================*/
/* Event handlers							  */
/*========================================================================*/

/******	toolboxe_objectautocreated() ******************************\

Purpose:	Handles Toolbox_ObjectAutoCreated - only the master
		"Joystick" window is auto-created (the detail templates are
		created on demand), but guard by a gadget only the master has
		anyway, in case a detail template ever gets Auto Created
		ticked by accident. Probes the API tiers, builds the list,
		opens the window.

\**************************************************************************/

static int toolboxe_objectautocreated (int event_code, ToolboxEvent *event, IdBlock *id_block, void *handle)
{
  static BOOL initialised = FALSE;
  BBox dummy;
  int i;

  /* Registered for object -1 ("any"). Only act for the master - identify
     it by a gadget only it has (the list) rather than assuming it's the
     first to fire. */
  if (gadget_get_bbox (0, id_block->self_id, joylist, &dummy) != NULL) return 1;

  if (initialised) return 1;
  initialised = TRUE;

  mainwindow_id = id_block->self_id;

  for (i = 0; i < MAX_DETAIL; i++) detail [i].obj = NULL_ObjectId;

  rescan_swis ();		/* initial probe; re-run periodically from the poll */
  setup_global_settings ();	/* seed + fade the Emulation/Control checkboxes */

  /* Nothing selected yet -> Edit faded. refresh_list() builds the rows;
     an empty list is exactly the "no joysticks" state (no None tab needed). */
  fade_gadget (mainwindow_id, joylist_edit, TRUE);
  refresh_list ();

  misc_openwindow (mainwindow_id, TRUE);
  return 1;
}

/******	toolboxe_scrolllistselection() ****************************\

Purpose:	Handles ScrollList_Selection from the master list. A plain
		select updates which row is current (and un-fades Edit); a
		double-click opens that row's detail window.

\**************************************************************************/

static int toolboxe_scrolllistselection (int event_code, ToolboxEvent *event, IdBlock *id_block, void *handle)
{
  ScrollListSelectionEvent *e = (ScrollListSelectionEvent *) event;
  int row = e->item;

  if (id_block->self_id != mainwindow_id || id_block->self_component != joylist) return 1;
  if (row < 0 || row >= row_count) return 1;

  sel_kind = row_map [row].kind;
  sel_index = row_map [row].index;
  fade_gadget (mainwindow_id, joylist_edit, FALSE);

  if (e->flags & ScrollList_Selection_Flags_DoubleClick)
    open_detail (sel_kind, sel_index);

  return 1;
}

/******	toolboxe_actionbuttonselected() ***************************\

Purpose:	Handles ActionButton_Selected. Master window: Edit opens the
		selected row; Set applies the Emulation/Control checkboxes'
		OWN current state live (Joystick_Emulate/Joystick_Control),
		persists with Joystick_SaveSettings, then quits - same
		apply-then-persist order as a DevDetail Set, and closes exactly
		like Close does, just after doing the work first; Close quits
		straight away, silently discarding any un-Set checkbox changes,
		since nothing was ever applied.
		DevDetail windows: Set writes the window's DRAFT out for real
		(Joystick_WriteBindings + SetStick, then Joystick_SaveMapping
		persists it) and closes; Cancel just closes, discarding the
		draft untouched - no SWI calls at all; Default resets the draft
		to the default-mapping preview (Joystick_ReadDefaultBindings)
		and re-renders, still without touching the module. See struct
		detail_win.

\**************************************************************************/

static int toolboxe_actionbuttonselected (int event_code, ToolboxEvent *event, IdBlock *id_block, void *handle)
{
  if (id_block->self_id == mainwindow_id)
  {
    switch (id_block->self_component)
    {
      case joylist_edit:
        if (sel_kind >= 0) open_detail (sel_kind, sel_index);
        break;
      case joylist_close:
        quit = TRUE;
        break;
      case joylist_set:
      {
        int state;

        if (have_emulate)
        {
          optionbutton_get_state (0, mainwindow_id, joylist_emu_serialport, &state);
          apply_flag (emulate_swi, JOYSTICK_EMULATE_SERIALPORT, state);
          optionbutton_get_state (0, mainwindow_id, joylist_emu_joy, &state);
          apply_flag (emulate_swi, JOYSTICK_EMULATE_JOY, state);
          optionbutton_get_state (0, mainwindow_id, joylist_emu_adc, &state);
          apply_flag (emulate_swi, JOYSTICK_EMULATE_ADC, state);
        }
        if (have_control)
        {
          optionbutton_get_state (0, mainwindow_id, joylist_ctrl_mouse, &state);
          apply_flag (control_swi, JOYSTICK_CONTROL_MOUSE, state);
          optionbutton_get_state (0, mainwindow_id, joylist_ctrl_keyboard, &state);
          apply_flag (control_swi, JOYSTICK_CONTROL_KEYBOARD, state);
        }
        if (have_savesettings)
        {
          _kernel_swi_regs regs;
          _kernel_swi (savesettings_swi, &regs, &regs);
        }
        quit = TRUE;		/* same as Close - the master window IS the app */
        break;
      }
    }
    return 1;
  }

  /* BindEdit dialogue buttons. OK/Delete/Cancel are hidden by the code (NOT by
     a ResEd auto-hide flag) so a rejected WriteBindings can keep the dialogue
     open with the error showing. */
  if (id_block->self_id == bindedit_obj)
  {
    ComponentId c = id_block->self_component;
    if      (c == bindedit_ok)     { if (bindedit_commit (bindedit_obj)) toolbox_hide_object (0, bindedit_obj); }
    else if (c == bindedit_delete) { if (bindedit_delete_binding ())     toolbox_hide_object (0, bindedit_obj); }
    else if (c == bindedit_cancel) toolbox_hide_object (0, bindedit_obj);
    return 1;
  }

  /* DevDetail buttons. Nothing an axis/button/"Mapped to" edit does ever
     touches the module - it all lands in d->draft_* (see struct detail_win).
     The footer is the only place that talks to (USB)Joystick:
       - Set     -> Joystick_WriteBindings (the draft table) then
                    Joystick_SetStick (the draft legacy stick), then
                    Joystick_SaveMapping persists it; closes on success. On a
                    WriteBindings validation error the window is left open,
                    draft intact, so the user can fix and retry - the same
                    "report and keep open" pattern BindEdit itself used to
                    use when it wrote live.
       - Cancel  -> close the window. The draft is simply thrown away with
                    it; since nothing was ever applied, there is nothing to
                    undo.
       - Default -> Joystick_ReadDefaultBindings (a read-only PREVIEW) - see
                    replaces the draft and the window is re-rendered from it.
                    Still doesn't touch the module.
     The Set/Cancel/Default buttons live in the bottom tool-bar PANE, so their
     events arrive with self_id = the pane and ancestor_id = the DevDetail
     window; the axis "..." buttons are in the work area, so their self_id is
     the DevDetail window itself. Resolve the owning slot from whichever
     matches, and close the parent in code (the pane has no auto-hide). */
  {
    struct detail_win *d = find_detail_by_obj (id_block->self_id);
    BOOL from_footer = FALSE;
    if (d == NULL)
    {
      d = find_detail_by_footer (id_block->self_id);		/* pane -> its window */
      if (d == NULL) d = find_detail_by_obj (id_block->ancestor_id);
      if (d == NULL) d = find_detail_by_obj (id_block->parent_id);
      from_footer = (d != NULL);
    }

    if (d != NULL && d->kind == KIND_DEVICE)
    {
      ComponentId comp = id_block->self_component;

      if (from_footer && comp == devdetail_set)
      {
        BOOL ok = TRUE;

        if (have_writebindings)
        {
          _kernel_swi_regs regs;
          _kernel_oserror *e;
          regs.r[0] = d->index;
          regs.r[1] = (int) d->draft_bindings;
          regs.r[2] = d->draft_count;
          e = _kernel_swi (writebindings_swi, &regs, &regs);
          if (e != NULL) { wimp_report_error (e, 1, "JoySetup"); ok = FALSE; }
        }

        if (ok && have_setstick)
        {
          _kernel_swi_regs regs;
          regs.r[0] = d->index;
          regs.r[1] = d->draft_stick;
          _kernel_swi (setstick_swi, &regs, &regs);
        }

        if (ok && have_save)
        {
          _kernel_swi_regs regs;
          regs.r[0] = d->index;
          regs.r[1] = 0;		/* profile 0 = default (reserved) */
          _kernel_swi (save_swi, &regs, &regs);
        }

        if (ok) toolbox_hide_object (0, d->obj);
      }
      else if (from_footer && comp == devdetail_cancel)
        toolbox_hide_object (0, d->obj);
      else if (from_footer && comp == devdetail_default)
      {
        int stick = JOY_NOT_MAPPED;
        int count = read_default_bindings (d->index, d->draft_bindings, JB_MAX_BINDINGS, &stick);
        if (count >= 0)
        {
          d->draft_count = count;
          d->draft_stick = stick;
          refresh_device_detail (d);
        }
      }
      /* The per-axis Edit/Add pickers are StringSets - handled in
         toolboxe_stringsetchanged, not here. */
    }
  }
  return 1;
}

/******	find_source_binding() ************************************\

Purpose:	Table index of the binding driven by (src_type, src_index) whose
		target text matches `text` (as an Edit StringSet lists it), or -1.
		Turns an Edit-picker selection back into the binding to edit.
		Searches the DRAFT table handed in, not a fresh live read.

\**************************************************************************/

static int find_source_binding (const joy_binding *table, int count, int src_type, int src_index, const char *text)
{
  int i;

  for (i = 0; i < count; i++)
  {
    char one [40];
    if (table [i].source_type != src_type || (int) table [i].source_index != src_index) continue;
    format_target (&table [i], one);
    if (strcmp (one, text) == 0) return i;
  }
  return -1;
}

/******	toolboxe_stringsetchanged() *******************************\

Purpose:	Handles StringSet_ValueChanged. BindEdit's target-kind picker
		reshapes its detail fields. On a DevDetail window: "Mapped to"
		updates the window's draft legacy-stick number (see struct
		detail_win - nothing here reaches the module); a per-axis Edit
		picker opens BindEdit on the chosen binding; a per-axis Add
		picker opens BindEdit on a new binding for the chosen target.
		The Edit/Add pickers are reset to their placeholder after acting.

		Needs the StringSet's "Generate value changed" ticked in ResEd
		or it never fires on a user pick.

\**************************************************************************/

static int toolboxe_stringsetchanged (int event_code, ToolboxEvent *event, IdBlock *id_block, void *handle)
{
  struct detail_win *d;
  ComponentId comp = id_block->self_component;

  /* BindEdit's target-kind picker: reconfigure the detail/number fields for
     the newly chosen kind. */
  if (id_block->self_id == bindedit_obj && comp == bindedit_kind)
  {
    char *s = ((StringSetValueChangedEvent *) event)->string;
    int i;
    for (i = 0; i < 7; i++)
      if (strcmp (s, kind_names [i]) == 0) { bindedit_show_kind (bindedit_obj, kind_values [i]); break; }
    return 1;
  }

  d = find_detail_by_obj (id_block->self_id);
  if (d == NULL || d->kind != KIND_DEVICE) return 1;

  if (comp == devdetail_stick)
  {
    char *s = ((StringSetValueChangedEvent *) event)->string;
    d->draft_stick = (strncmp (s, "Stick ", 6) == 0) ? atoi (s + 6) : JOY_NOT_MAPPED;
    return 1;
  }

  /* Per-axis Edit picker (0x203 + n*0x10): pick one of the axis's bindings to
     edit. */
  if (comp >= devdetail_axis_edit (0) && comp <= devdetail_axis_edit (AXIS_MAX_DISPLAY - 1) &&
      ((comp - devdetail_axis_edit (0)) % 0x10) == 0)
  {
    char *s = ((StringSetValueChangedEvent *) event)->string;
    int axis = (comp - devdetail_axis_edit (0)) / 0x10;
    int idx  = find_source_binding (d->draft_bindings, d->draft_count, JB_SRC_AXIS, axis, s);

    if (idx >= 0) bindedit_open_edit (d->obj, d->index, JB_SRC_AXIS, axis, idx);
    stringset_set_selected (0, d->obj, comp, "");		/* clear the shown value */
    return 1;
  }

  /* Per-axis Add picker (0x204 + n*0x10): pick an offered target to add. */
  if (comp >= devdetail_axis_add (0) && comp <= devdetail_axis_add (AXIS_MAX_DISPLAY - 1) &&
      ((comp - devdetail_axis_add (0)) % 0x10) == 0)
  {
    char *s = ((StringSetValueChangedEvent *) event)->string;
    int axis = (comp - devdetail_axis_add (0)) / 0x10;
    const struct add_target *a = parse_add_label (s);

    if (a != NULL) bindedit_open_add (d->obj, d->index, JB_SRC_AXIS, axis, a->kind, a->index);
    stringset_set_selected (0, d->obj, comp, "");		/* clear the shown value */
    return 1;
  }

  /* Button-bindings row: which-button picker just repopulates the row; its Edit
     and Add pickers open BindEdit for the chosen button source. */
  if (comp == devdetail_btnpick)
  {
    refresh_button_row (d->obj, d->draft_bindings, d->draft_count, selected_button (d->obj));
    return 1;
  }
  if (comp == devdetail_btnedit)
  {
    char *s = ((StringSetValueChangedEvent *) event)->string;
    int btn = selected_button (d->obj);
    int idx = find_source_binding (d->draft_bindings, d->draft_count, JB_SRC_BUTTON, btn, s);

    if (btn >= 0 && idx >= 0) bindedit_open_edit (d->obj, d->index, JB_SRC_BUTTON, btn, idx);
    stringset_set_selected (0, d->obj, comp, "");
    return 1;
  }
  if (comp == devdetail_btnadd)
  {
    char *s = ((StringSetValueChangedEvent *) event)->string;
    int btn = selected_button (d->obj);
    const struct add_target *a = parse_add_label (s);

    if (btn >= 0 && a != NULL) bindedit_open_add (d->obj, d->index, JB_SRC_BUTTON, btn, a->kind, a->index);
    stringset_set_selected (0, d->obj, comp, "");
    return 1;
  }

  return 1;
}

/******	toolboxe_poll() *******************************************\

Purpose:	Called on every null Wimp event (see WimpE.c). Throttled to
		POLL_INTERVAL centiseconds. Notices device attach/detach
		(refresh_list) and refreshes every open detail window's live
		view from its own bound stick/slot. Any window we still track
		is showing - a hidden one has already been deleted via
		Window_HasBeenHidden.

\**************************************************************************/

void toolboxe_poll (void)
{
  _kernel_swi_regs regs;
  int now, i;

  if (mainwindow_id == NULL_ObjectId) return;

  if (_kernel_swi (OS_ReadMonotonicTime, &regs, &regs) != NULL) return;
  now = regs.r[0];

  /* Slow rescan (its own throttle): re-probe the SWIs to notice a provider
     module loaded/killed since startup. If a tier changed it clears
     list_built, and the fast poll's refresh_list (below) rebuilds the list. */
  if (now - next_rescan_due >= 0)
  {
    next_rescan_due = now + RESCAN_INTERVAL;
    rescan_swis ();
  }

  if (now - next_poll_due < 0) return;		/* fast poll not due yet */
  next_poll_due = now + POLL_INTERVAL;

  refresh_list ();

  for (i = 0; i < MAX_DETAIL; i++)
    if (detail [i].obj != NULL_ObjectId)
    {
      if (detail [i].kind == KIND_STICK) poll_stick_detail (&detail [i]);
      else                               poll_device_detail (&detail [i]);
    }
}

/******	toolboxe_button_click() ***********************************\

Purpose:	Handle a Wimp mouse click reported on a DevDetail button
		indicator (devdetail_btn(n) = 0x400 + n): pre-select that button
		in the button-bindings picker and show its row, so clicking a
		button in the grid jumps straight to editing it. Returns 1 if the
		click was on a button we handled, 0 otherwise (pass on).

		Called from WimpE's mouse-click handler - the button indicators
		are display gadgets that raise no Toolbox event of their own.

\**************************************************************************/

int toolboxe_button_click (ObjectId obj, ComponentId comp)
{
  struct detail_win *d;
  char buf [8];
  int n;

  if (comp < devdetail_btn (0) || comp > devdetail_btn (BUTTON_MAX_DISPLAY - 1)) return 0;

  d = find_detail_by_obj (obj);
  if (d == NULL || d->kind != KIND_DEVICE) return 0;

  n = comp - devdetail_btn (0);
  sprintf (buf, "%d", n);
  stringset_set_selected (0, obj, devdetail_btnpick, buf);
  refresh_button_row (obj, d->draft_bindings, d->draft_count, n);
  return 1;
}
