737 lines
23 KiB
C
737 lines
23 KiB
C
// The MIT License (MIT)
|
|
//
|
|
// Copyright (C) 2026 Scott Duensing
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to
|
|
// deal in the Software without restriction, including without limitation the
|
|
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
// sell copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
// IN THE SOFTWARE.
|
|
|
|
// cpJoystick.c -- Control Panel's Game Controllers page (see cpJoystick.h).
|
|
//
|
|
// The joystick library is looked up by name when the page is built, so
|
|
// the Control Panel still works where it is not installed (the page then
|
|
// says so). The page holds a handle on the chosen controller: a timer
|
|
// redraws the test view from it, and the calibration wizard reads its raw
|
|
// values. Every change (calibration, dead zone, game port mode, keyboard
|
|
// joystick) applies at once through the library; OK stores the settings
|
|
// in DVX.INI, Cancel loads them back from the copy read when the Control
|
|
// Panel opened.
|
|
|
|
#include "cpJoystick.h"
|
|
#include "joystick/joystick.h"
|
|
#include "dvxDlg.h"
|
|
#include "dvxPlat.h"
|
|
#include "box/box.h"
|
|
#include "button/button.h"
|
|
#include "canvas/canvas.h"
|
|
#include "dropdown/dropdown.h"
|
|
#include "label/label.h"
|
|
#include "slider/slider.h"
|
|
#include "timer/timer.h"
|
|
#include "stb_ds_wrap.h"
|
|
|
|
#include <dlfcn.h>
|
|
#include <inttypes.h>
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#define CPJ_LABEL_W 90
|
|
#define CPJ_GAP 4
|
|
#define CPJ_BTN_W 110
|
|
#define CPJ_SMALL_BTN_W 70
|
|
#define CPJ_TICK_MS 50
|
|
#define CPJ_VIEW_W 320
|
|
#define CPJ_VIEW_H 128
|
|
#define CPJ_BOX 128 // the X / Y square
|
|
#define CPJ_DOT_R 3
|
|
#define CPJ_BAR_X 144 // axis bars, buttons and hat, right of the square
|
|
#define CPJ_BAR_W 140
|
|
#define CPJ_BAR_H 10
|
|
#define CPJ_ROW_H 18
|
|
#define CPJ_SHOWN_AXES 4
|
|
#define CPJ_BTN_Y (CPJ_SHOWN_AXES * CPJ_ROW_H + CPJ_GAP)
|
|
#define CPJ_BTN_BOX 18
|
|
#define CPJ_SHOWN_BTNS 8
|
|
#define CPJ_HAT_Y (CPJ_BTN_Y + CPJ_BTN_BOX + CPJ_GAP)
|
|
#define CPJ_STATUS_Y (CPJ_HAT_Y + CPJ_ROW_H)
|
|
#define CPJ_ZONE_LBL_W 40
|
|
#define CPJ_TEXT_BUF 128
|
|
#define CPJ_KEY_BUF 32
|
|
#define CPJ_KEYS_DLG_W 240
|
|
#define CPJ_WIZ_DLG_W 470
|
|
#define CPJ_WIZ_CHARS 56 // the wizard's text lines keep this width as their text changes
|
|
#define CPJ_SPAN_DIV 4 // each side of an axis's centre must reach centre / 4 away
|
|
#define CPJ_AXIS_SPAN ((int64_t)JOY_AXIS_MAX - JOY_AXIS_MIN)
|
|
|
|
// The joystick library's functions (NULL when it is not installed)
|
|
typedef struct {
|
|
__typeof__(joyChoiceGet) *choiceGet;
|
|
__typeof__(joyChoices) *choices;
|
|
__typeof__(joyChoiceSet) *choiceSet;
|
|
__typeof__(joyClose) *close;
|
|
__typeof__(joyCount) *count;
|
|
__typeof__(joyGetDeadZone) *getDeadZone;
|
|
__typeof__(joyGetInfo) *getInfo;
|
|
__typeof__(joyGetRaw) *getRaw;
|
|
__typeof__(joyKbdSlotGet) *slotGet;
|
|
__typeof__(joyKbdSlotSet) *slotSet;
|
|
__typeof__(joyKeyCount) *keyCount;
|
|
__typeof__(joyKeyName) *keyName;
|
|
__typeof__(joyOpen) *open;
|
|
__typeof__(joyRead) *read;
|
|
__typeof__(joyRescan) *rescan;
|
|
__typeof__(joySetCalibration) *setCal;
|
|
__typeof__(joySetDeadZone) *setDeadZone;
|
|
__typeof__(joySettingsLoad) *load;
|
|
__typeof__(joySettingsStore) *store;
|
|
} JoyFnsT;
|
|
|
|
typedef struct {
|
|
const char *name;
|
|
size_t offset;
|
|
} JoyFnSymT;
|
|
|
|
// The calibration wizard's steps
|
|
typedef enum {
|
|
WizCenterE,
|
|
WizEdgesE,
|
|
WizButtonsE
|
|
} WizStepE;
|
|
|
|
typedef struct {
|
|
WindowT *dlg;
|
|
WidgetT *text;
|
|
WidgetT *values;
|
|
WidgetT *next;
|
|
JoyInfoT info;
|
|
JoyAxisCalT cal[JOY_MAX_AXES];
|
|
WizStepE step;
|
|
} CalWizT;
|
|
|
|
// ============================================================
|
|
// Tables
|
|
// ============================================================
|
|
|
|
#define JOY_FN_SYM(field, fn) { DVX_SYM(#fn), offsetof(JoyFnsT, field) }
|
|
|
|
static const JoyFnSymT sSyms[] = {
|
|
JOY_FN_SYM(choiceGet, joyChoiceGet),
|
|
JOY_FN_SYM(choices, joyChoices),
|
|
JOY_FN_SYM(choiceSet, joyChoiceSet),
|
|
JOY_FN_SYM(close, joyClose),
|
|
JOY_FN_SYM(count, joyCount),
|
|
JOY_FN_SYM(getDeadZone, joyGetDeadZone),
|
|
JOY_FN_SYM(getInfo, joyGetInfo),
|
|
JOY_FN_SYM(getRaw, joyGetRaw),
|
|
JOY_FN_SYM(slotGet, joyKbdSlotGet),
|
|
JOY_FN_SYM(slotSet, joyKbdSlotSet),
|
|
JOY_FN_SYM(keyCount, joyKeyCount),
|
|
JOY_FN_SYM(keyName, joyKeyName),
|
|
JOY_FN_SYM(open, joyOpen),
|
|
JOY_FN_SYM(read, joyRead),
|
|
JOY_FN_SYM(rescan, joyRescan),
|
|
JOY_FN_SYM(setCal, joySetCalibration),
|
|
JOY_FN_SYM(setDeadZone, joySetDeadZone),
|
|
JOY_FN_SYM(load, joySettingsLoad),
|
|
JOY_FN_SYM(store, joySettingsStore)
|
|
};
|
|
|
|
static const char *const sAxisNames[CPJ_SHOWN_AXES] = { "X", "Y", "3", "4" };
|
|
|
|
static const char *const sSlotLabels[JOY_KBD_SLOT_COUNT] = { "Up:", "Down:", "Left:", "Right:", "Button 1:", "Button 2:", "Button 3:", "Button 4:" };
|
|
|
|
static const char *const sWizText[] = {
|
|
"Let the stick rest in the middle, then click Next.",
|
|
"Move the stick round all its edges, then click Next.",
|
|
"Press each button to test it, then click Finish."
|
|
};
|
|
|
|
// ============================================================
|
|
// Module state
|
|
// ============================================================
|
|
|
|
static JoyFnsT sJoy;
|
|
static AppContextT *sAc = NULL;
|
|
static PrefsHandleT *sPrefs = NULL;
|
|
static WidgetT *sDevDrop = NULL;
|
|
static WidgetT *sView = NULL;
|
|
static WidgetT *sCalBtn = NULL;
|
|
static WidgetT *sResetBtn = NULL;
|
|
static WidgetT *sZoneSldr = NULL;
|
|
static WidgetT *sZoneLbl = NULL;
|
|
static WidgetT *sModeDrop = NULL;
|
|
static WidgetT *sKbdDrop = NULL;
|
|
static WidgetT *sKeyDrops[JOY_KBD_SLOT_COUNT];
|
|
static const char **sKeyItems = NULL; // stb_ds array of the key names
|
|
static const char *sModeLabels[JOY_GAMEPORT_CHOICE_COUNT];
|
|
static const char *sFallbackLabels[JOY_FALLBACK_CHOICE_COUNT];
|
|
static int32_t sDevice = -1;
|
|
static int32_t sHandle = -1;
|
|
static JoyStateT sShown;
|
|
static bool sShownValid = false;
|
|
static char sDevNames[JOY_MAX_DEVICES][JOY_NAME_MAX];
|
|
static const char *sDevLabels[JOY_MAX_DEVICES];
|
|
static CalWizT sWiz;
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static WidgetT *addRow(WidgetT *parent, const char *label);
|
|
static int32_t axisPixel(int32_t value, int32_t size);
|
|
static void choiceDrop(WidgetT *drop, JoyChoiceE which, const char **labels);
|
|
void cpJoystickBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs);
|
|
void cpJoystickFree(void);
|
|
void cpJoystickRevert(void);
|
|
void cpJoystickStore(PrefsHandleT *prefs);
|
|
static void drawView(void);
|
|
static bool joyStateEqual(const JoyStateT *a, const JoyStateT *b);
|
|
static void onCalibrate(WidgetT *w);
|
|
static void onDetect(WidgetT *w);
|
|
static void onDevice(WidgetT *w);
|
|
static void onFallback(WidgetT *w);
|
|
static void onKeys(WidgetT *w);
|
|
static bool onKeysOk(WindowT *dlg, void *userCtx);
|
|
static void onMode(WidgetT *w);
|
|
static void onReset(WidgetT *w);
|
|
static void onTick(WidgetT *w);
|
|
static void onWizCancel(WidgetT *w);
|
|
static void onWizNext(WidgetT *w);
|
|
static void onWizTick(WidgetT *w);
|
|
static void onZone(WidgetT *w);
|
|
static void refreshDevices(void);
|
|
static bool resolveLibrary(void);
|
|
static void selectDevice(int32_t device);
|
|
static void showZone(void);
|
|
static bool wizAxis(int32_t axis);
|
|
|
|
|
|
// "Label:" and the row for its controls.
|
|
static WidgetT *addRow(WidgetT *parent, const char *label) {
|
|
WidgetT *row = wgtHBox(parent);
|
|
WidgetT *lbl = wgtLabel(row, label);
|
|
|
|
row->spacing = wgtPixels(CPJ_GAP);
|
|
lbl->prefW = wgtPixels(CPJ_LABEL_W);
|
|
return row;
|
|
}
|
|
|
|
|
|
// An axis value as a pixel offset 0..size-1.
|
|
static int32_t axisPixel(int32_t value, int32_t size) {
|
|
return (int32_t)(((int64_t)value - JOY_AXIS_MIN) * (size - 1) / CPJ_AXIS_SPAN);
|
|
}
|
|
|
|
|
|
// A listed [joystick] setting's choices in drop, its current one chosen.
|
|
static void choiceDrop(WidgetT *drop, JoyChoiceE which, const char **labels) {
|
|
int32_t count = 0;
|
|
const PrefsChoiceT *choices = sJoy.choices(which, &count);
|
|
|
|
prefsChoiceLabels(choices, count, labels);
|
|
wgtDropdownSetItems(drop, labels, count);
|
|
wgtDropdownSetSelected(drop, sJoy.choiceGet(which));
|
|
}
|
|
|
|
|
|
void cpJoystickBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
|
|
sAc = ac;
|
|
sPrefs = prefs;
|
|
sDevice = -1;
|
|
sHandle = -1;
|
|
sShownValid = false;
|
|
|
|
if (!resolveLibrary()) {
|
|
wgtLabel(page, "Game controller support is not installed.");
|
|
return;
|
|
}
|
|
|
|
WidgetT *row = addRow(page, "Controller:");
|
|
|
|
sDevDrop = wgtDropdown(row);
|
|
sDevDrop->weight = WGT_WEIGHT_FILL;
|
|
sDevDrop->onChange = onDevice;
|
|
|
|
WidgetT *detect = wgtButton(row, "Detect");
|
|
|
|
detect->onClick = onDetect;
|
|
detect->prefW = wgtPixels(CPJ_SMALL_BTN_W);
|
|
sView = wgtCanvas(page, CPJ_VIEW_W, CPJ_VIEW_H);
|
|
row = wgtHBox(page);
|
|
row->spacing = wgtPixels(CPJ_GAP);
|
|
sCalBtn = wgtButton(row, "Calibrate...");
|
|
sCalBtn->onClick = onCalibrate;
|
|
sCalBtn->prefW = wgtPixels(CPJ_BTN_W);
|
|
sResetBtn = wgtButton(row, "Automatic");
|
|
sResetBtn->onClick = onReset;
|
|
sResetBtn->prefW = wgtPixels(CPJ_BTN_W);
|
|
|
|
wgtLabel(row, "Dead zone:");
|
|
sZoneSldr = wgtSlider(row, 0, JOY_DEADZONE_MAX);
|
|
sZoneSldr->weight = WGT_WEIGHT_FILL;
|
|
sZoneSldr->onChange = onZone;
|
|
sZoneLbl = wgtLabel(row, "");
|
|
sZoneLbl->prefW = wgtPixels(CPJ_ZONE_LBL_W);
|
|
|
|
row = addRow(page, "Game port:");
|
|
sModeDrop = wgtDropdown(row);
|
|
sModeDrop->onChange = onMode;
|
|
choiceDrop(sModeDrop, JoyChoiceGamePortE, sModeLabels);
|
|
|
|
row = addRow(page, "Keyboard:");
|
|
sKbdDrop = wgtDropdown(row);
|
|
sKbdDrop->onChange = onFallback;
|
|
choiceDrop(sKbdDrop, JoyChoiceFallbackE, sFallbackLabels);
|
|
|
|
WidgetT *keys = wgtButton(row, "Keys...");
|
|
|
|
keys->onClick = onKeys;
|
|
keys->prefW = wgtPixels(CPJ_SMALL_BTN_W);
|
|
|
|
WidgetT *timer = wgtTimer(page, CPJ_TICK_MS, true);
|
|
|
|
timer->onChange = onTick;
|
|
refreshDevices();
|
|
}
|
|
|
|
|
|
void cpJoystickFree(void) {
|
|
if (sJoy.close) {
|
|
sJoy.close(sHandle);
|
|
}
|
|
|
|
sHandle = -1;
|
|
arrfree(sKeyItems);
|
|
sKeyItems = NULL;
|
|
}
|
|
|
|
|
|
void cpJoystickRevert(void) {
|
|
if (sJoy.load) {
|
|
sJoy.load(sPrefs);
|
|
}
|
|
}
|
|
|
|
|
|
void cpJoystickStore(PrefsHandleT *prefs) {
|
|
if (sJoy.store) {
|
|
sJoy.store(prefs);
|
|
}
|
|
}
|
|
|
|
|
|
// The test view: the X / Y position in the square, a bar per axis, the
|
|
// buttons (lit while down), the hat, and whether it answers.
|
|
static void drawView(void) {
|
|
const ColorSchemeT *cs = &sAc->colors;
|
|
JoyInfoT info;
|
|
|
|
wgtCanvasClear(sView, cs->windowFace);
|
|
wgtCanvasSetPenColor(sView, cs->contentFg);
|
|
|
|
if (sDevice < 0 || !sJoy.getInfo(sDevice, &info)) {
|
|
wgtCanvasDrawText(sView, 0, 0, "No game controllers found.");
|
|
return;
|
|
}
|
|
|
|
if (sShownValid && !sShown.connected) {
|
|
wgtCanvasDrawText(sView, CPJ_BAR_X, CPJ_STATUS_Y, "Not connected");
|
|
}
|
|
|
|
wgtCanvasSetPenColor(sView, cs->contentBg);
|
|
wgtCanvasFillRect(sView, 0, 0, CPJ_BOX, CPJ_BOX);
|
|
wgtCanvasSetPenColor(sView, cs->windowShadow);
|
|
wgtCanvasDrawRect(sView, 0, 0, CPJ_BOX, CPJ_BOX);
|
|
wgtCanvasDrawLine(sView, CPJ_BOX / 2, 0, CPJ_BOX / 2, CPJ_BOX - 1);
|
|
wgtCanvasDrawLine(sView, 0, CPJ_BOX / 2, CPJ_BOX - 1, CPJ_BOX / 2);
|
|
wgtCanvasSetPenColor(sView, cs->activeTitleBg);
|
|
wgtCanvasFillCircle(sView, axisPixel(sShown.axes[0], CPJ_BOX), axisPixel(sShown.axes[1], CPJ_BOX), CPJ_DOT_R);
|
|
|
|
int32_t barX = CPJ_BAR_X + sAc->font.charWidth * 2;
|
|
|
|
for (int32_t a = 0; a < info.axisCount && a < CPJ_SHOWN_AXES; a++) {
|
|
int32_t y = a * CPJ_ROW_H;
|
|
|
|
wgtCanvasSetPenColor(sView, cs->contentFg);
|
|
wgtCanvasDrawText(sView, CPJ_BAR_X, y, sAxisNames[a]);
|
|
wgtCanvasSetPenColor(sView, cs->contentBg);
|
|
wgtCanvasFillRect(sView, barX, y + CPJ_GAP, CPJ_BAR_W, CPJ_BAR_H);
|
|
|
|
if (info.axisPresent & (1U << a)) {
|
|
wgtCanvasSetPenColor(sView, cs->activeTitleBg);
|
|
wgtCanvasFillRect(sView, barX, y + CPJ_GAP, axisPixel(sShown.axes[a], CPJ_BAR_W) + 1, CPJ_BAR_H);
|
|
}
|
|
|
|
wgtCanvasSetPenColor(sView, cs->windowShadow);
|
|
wgtCanvasDrawRect(sView, barX, y + CPJ_GAP, CPJ_BAR_W, CPJ_BAR_H);
|
|
}
|
|
|
|
for (int32_t b = 0; b < info.buttonCount && b < CPJ_SHOWN_BTNS; b++) {
|
|
int32_t x = CPJ_BAR_X + b * (CPJ_BTN_BOX + CPJ_GAP);
|
|
bool down = (sShown.buttons & (1U << b)) != 0;
|
|
char digit[2];
|
|
|
|
digit[0] = (char)('1' + b);
|
|
digit[1] = '\0';
|
|
wgtCanvasSetPenColor(sView, down ? cs->activeTitleBg : cs->contentBg);
|
|
wgtCanvasFillRect(sView, x, CPJ_BTN_Y, CPJ_BTN_BOX, CPJ_BTN_BOX);
|
|
wgtCanvasSetPenColor(sView, cs->windowShadow);
|
|
wgtCanvasDrawRect(sView, x, CPJ_BTN_Y, CPJ_BTN_BOX, CPJ_BTN_BOX);
|
|
wgtCanvasSetPenColor(sView, down ? cs->activeTitleFg : cs->contentFg);
|
|
wgtCanvasDrawText(sView, x + (CPJ_BTN_BOX - sAc->font.charWidth) / 2, CPJ_BTN_Y + (CPJ_BTN_BOX - sAc->font.charHeight) / 2, digit);
|
|
}
|
|
|
|
if (info.hatCount > 0) {
|
|
char text[CPJ_TEXT_BUF];
|
|
|
|
if (sShown.hats[0] == JOY_HAT_CENTERED) {
|
|
snprintf(text, sizeof(text), "Hat: centred");
|
|
} else {
|
|
snprintf(text, sizeof(text), "Hat: %" PRId32 " degrees", sShown.hats[0]);
|
|
}
|
|
|
|
wgtCanvasSetPenColor(sView, cs->contentFg);
|
|
wgtCanvasDrawText(sView, CPJ_BAR_X, CPJ_HAT_Y, text);
|
|
}
|
|
}
|
|
|
|
|
|
// Two states the view draws alike, compared field by field: the struct
|
|
// ends in a bool, and memcmp would compare its padding too.
|
|
static bool joyStateEqual(const JoyStateT *a, const JoyStateT *b) {
|
|
return memcmp(a->axes, b->axes, sizeof(a->axes)) == 0 && memcmp(a->hats, b->hats, sizeof(a->hats)) == 0 && a->buttons == b->buttons && a->connected == b->connected;
|
|
}
|
|
|
|
|
|
// The wizard reads raw values through the page's handle, which keeps the
|
|
// polling going while the dialog is up.
|
|
static void onCalibrate(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (sDevice < 0 || !sJoy.getInfo(sDevice, &sWiz.info)) {
|
|
return;
|
|
}
|
|
|
|
sWiz.dlg = dvxDialogCreate(sAc, "Calibrate", CPJ_WIZ_DLG_W);
|
|
|
|
if (!sWiz.dlg) {
|
|
return;
|
|
}
|
|
|
|
WidgetT *root = sWiz.dlg->widgetRoot;
|
|
|
|
sWiz.step = WizCenterE;
|
|
sWiz.text = wgtLabel(root, sWizText[WizCenterE]);
|
|
sWiz.text->minW = wgtChars(CPJ_WIZ_CHARS);
|
|
sWiz.values = wgtLabel(root, "");
|
|
sWiz.values->minW = wgtChars(CPJ_WIZ_CHARS);
|
|
|
|
WidgetT *row = wgtHBox(root);
|
|
|
|
row->align = AlignEndE;
|
|
row->spacing = wgtPixels(CPJ_GAP);
|
|
sWiz.next = wgtButton(row, "Next");
|
|
sWiz.next->onClick = onWizNext;
|
|
sWiz.next->prefW = wgtPixels(CPJ_SMALL_BTN_W);
|
|
|
|
WidgetT *cancel = wgtButton(row, "Cancel");
|
|
|
|
cancel->onClick = onWizCancel;
|
|
cancel->prefW = wgtPixels(CPJ_SMALL_BTN_W);
|
|
|
|
WidgetT *timer = wgtTimer(root, CPJ_TICK_MS, true);
|
|
|
|
timer->onChange = onWizTick;
|
|
onWizTick(NULL);
|
|
dvxDialogRun(sAc, sWiz.dlg);
|
|
sWiz.dlg = NULL;
|
|
sShownValid = false;
|
|
}
|
|
|
|
|
|
// Look for controllers again. The page's own handle is closed first
|
|
// (with every handle of the Control Panel);
|
|
// another program's handle stops the search.
|
|
static void onDetect(WidgetT *w) {
|
|
(void)w;
|
|
sJoy.close(sHandle);
|
|
sHandle = -1;
|
|
|
|
if (sJoy.rescan() < 0) {
|
|
dvxMessageBox(sAc, "Game Controllers", "A program is using a game controller. Close it, then click Detect again.", MB_OK | MB_ICONWARNING);
|
|
}
|
|
|
|
refreshDevices();
|
|
}
|
|
|
|
|
|
static void onDevice(WidgetT *w) {
|
|
selectDevice(wgtDropdownGetSelected(w));
|
|
}
|
|
|
|
|
|
static void onFallback(WidgetT *w) {
|
|
sJoy.choiceSet(JoyChoiceFallbackE, wgtDropdownGetSelected(w));
|
|
onDetect(w);
|
|
}
|
|
|
|
|
|
// One dropdown of key names per keyboard joystick key.
|
|
static void onKeys(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (!sKeyItems) {
|
|
for (int32_t i = 0; i < sJoy.keyCount(); i++) {
|
|
arrput(sKeyItems, sJoy.keyName(i));
|
|
}
|
|
}
|
|
|
|
WindowT *dlg = dvxDialogCreate(sAc, "Keyboard Joystick", CPJ_KEYS_DLG_W);
|
|
|
|
if (!dlg) {
|
|
return;
|
|
}
|
|
|
|
for (int32_t i = 0; i < JOY_KBD_SLOT_COUNT; i++) {
|
|
sKeyDrops[i] = wgtDropdown(addRow(dlg->widgetRoot, sSlotLabels[i]));
|
|
wgtDropdownSetItems(sKeyDrops[i], sKeyItems, (int32_t)arrlen(sKeyItems));
|
|
wgtDropdownSetSelected(sKeyDrops[i], sJoy.slotGet(i));
|
|
}
|
|
|
|
dvxDialogAddOkCancel(dlg, "OK", onKeysOk, NULL);
|
|
dvxDialogRun(sAc, dlg);
|
|
}
|
|
|
|
|
|
static bool onKeysOk(WindowT *dlg, void *userCtx) {
|
|
(void)dlg;
|
|
(void)userCtx;
|
|
|
|
for (int32_t i = 0; i < JOY_KBD_SLOT_COUNT; i++) {
|
|
sJoy.slotSet(i, wgtDropdownGetSelected(sKeyDrops[i]));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// The game port makes its joysticks anew in the other mode.
|
|
static void onMode(WidgetT *w) {
|
|
sJoy.choiceSet(JoyChoiceGamePortE, wgtDropdownGetSelected(w));
|
|
onDetect(w);
|
|
}
|
|
|
|
|
|
// Forget the calibration: every axis ranges automatically again.
|
|
static void onReset(WidgetT *w) {
|
|
(void)w;
|
|
|
|
for (int32_t a = 0; a < JOY_MAX_AXES; a++) {
|
|
sJoy.setCal(sDevice, a, NULL);
|
|
}
|
|
}
|
|
|
|
|
|
// Redraw the view when the state changed.
|
|
static void onTick(WidgetT *w) {
|
|
JoyStateT st;
|
|
|
|
(void)w;
|
|
|
|
if (!sJoy.read(sHandle, &st) || (sShownValid && joyStateEqual(&st, &sShown))) {
|
|
return;
|
|
}
|
|
|
|
sShown = st;
|
|
sShownValid = true;
|
|
drawView();
|
|
}
|
|
|
|
|
|
static void onWizCancel(WidgetT *w) {
|
|
(void)w;
|
|
dvxDialogEnd(sWiz.dlg, false);
|
|
}
|
|
|
|
|
|
// Centre: every axis's resting value. Edges: the stick must have gone
|
|
// well out to both sides of the centre on every axis. Buttons: store.
|
|
static void onWizNext(WidgetT *w) {
|
|
JoyRawT raw;
|
|
|
|
(void)w;
|
|
|
|
switch (sWiz.step) {
|
|
case WizCenterE:
|
|
sJoy.getRaw(sDevice, &raw);
|
|
|
|
for (int32_t a = 0; a < sWiz.info.axisCount; a++) {
|
|
sWiz.cal[a].min = raw.axes[a];
|
|
sWiz.cal[a].center = raw.axes[a];
|
|
sWiz.cal[a].max = raw.axes[a];
|
|
}
|
|
|
|
sWiz.step = WizEdgesE;
|
|
break;
|
|
|
|
case WizEdgesE:
|
|
for (int32_t a = 0; a < sWiz.info.axisCount; a++) {
|
|
int32_t span = sWiz.cal[a].center / CPJ_SPAN_DIV;
|
|
|
|
if (wizAxis(a) && (sWiz.cal[a].center - sWiz.cal[a].min <= span || sWiz.cal[a].max - sWiz.cal[a].center <= span)) {
|
|
wgtSetText(sWiz.text, "Move the stick further, all the way to each edge.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
sWiz.step = WizButtonsE;
|
|
wgtSetText(sWiz.next, "Finish");
|
|
break;
|
|
|
|
case WizButtonsE:
|
|
for (int32_t a = 0; a < sWiz.info.axisCount; a++) {
|
|
if (wizAxis(a)) {
|
|
sJoy.setCal(sDevice, a, &sWiz.cal[a]);
|
|
}
|
|
}
|
|
|
|
dvxDialogEnd(sWiz.dlg, true);
|
|
return;
|
|
}
|
|
|
|
wgtSetText(sWiz.text, sWizText[sWiz.step]);
|
|
}
|
|
|
|
|
|
// The raw values (and, while the edges are being found, their ranges) and
|
|
// the buttons held.
|
|
static void onWizTick(WidgetT *w) {
|
|
JoyRawT raw;
|
|
char text[CPJ_TEXT_BUF];
|
|
int32_t pos = 0;
|
|
|
|
(void)w;
|
|
|
|
if (!sJoy.getRaw(sDevice, &raw)) {
|
|
return;
|
|
}
|
|
|
|
for (int32_t a = 0; a < sWiz.info.axisCount; a++) {
|
|
if (!wizAxis(a)) {
|
|
continue;
|
|
}
|
|
|
|
if (sWiz.step == WizEdgesE) {
|
|
JoyAxisCalT *c = &sWiz.cal[a];
|
|
|
|
c->min = raw.axes[a] < c->min ? raw.axes[a] : c->min;
|
|
c->max = raw.axes[a] > c->max ? raw.axes[a] : c->max;
|
|
pos = dvxStrAppendf(text, sizeof(text), pos, "%" PRId32 ": %" PRId32 "-%" PRId32 " ", a + 1, c->min, c->max);
|
|
} else {
|
|
pos = dvxStrAppendf(text, sizeof(text), pos, "%" PRId32 ": %" PRId32 " ", a + 1, raw.axes[a]);
|
|
}
|
|
}
|
|
|
|
pos = dvxStrAppendf(text, sizeof(text), pos, " Buttons:");
|
|
|
|
for (int32_t b = 0; b < sWiz.info.buttonCount; b++) {
|
|
pos = dvxStrAppendf(text, sizeof(text), pos, " %c", (raw.buttons & (1U << b)) ? (char)('1' + b) : '-');
|
|
}
|
|
|
|
if (pos >= 0) {
|
|
wgtSetText(sWiz.values, text);
|
|
}
|
|
}
|
|
|
|
|
|
static void onZone(WidgetT *w) {
|
|
sJoy.setDeadZone(sDevice, wgtSliderGetValue(w));
|
|
showZone();
|
|
}
|
|
|
|
|
|
// The controllers found, in the dropdown; the first one chosen.
|
|
static void refreshDevices(void) {
|
|
JoyInfoT info;
|
|
int32_t count = sJoy.count();
|
|
|
|
for (int32_t i = 0; i < count; i++) {
|
|
sJoy.getInfo(i, &info);
|
|
snprintf(sDevNames[i], sizeof(sDevNames[i]), "%s", info.name);
|
|
sDevLabels[i] = sDevNames[i];
|
|
}
|
|
|
|
wgtDropdownSetItems(sDevDrop, sDevLabels, count);
|
|
wgtDropdownSetSelected(sDevDrop, count > 0 ? 0 : -1);
|
|
selectDevice(count > 0 ? 0 : -1);
|
|
}
|
|
|
|
|
|
static bool resolveLibrary(void) {
|
|
for (int32_t i = 0; i < DVX_ARRAY_LEN(sSyms); i++) {
|
|
void *fn = dlsym(RTLD_DEFAULT, sSyms[i].name);
|
|
|
|
if (!fn) {
|
|
memset(&sJoy, 0, sizeof(sJoy));
|
|
return false;
|
|
}
|
|
|
|
memcpy((char *)&sJoy + sSyms[i].offset, &fn, sizeof(fn));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// Watch another controller: a handle on it, its dead zone, and the
|
|
// calibration buttons only for one with analog axes.
|
|
static void selectDevice(int32_t device) {
|
|
JoyInfoT info;
|
|
bool analog = device >= 0 && sJoy.getInfo(device, &info) && (info.caps & JOY_CAP_CALIBRATE);
|
|
|
|
sJoy.close(sHandle);
|
|
sDevice = device;
|
|
sHandle = device >= 0 ? sJoy.open(sAc, device) : -1;
|
|
sShownValid = false;
|
|
wgtSetEnabled(sCalBtn, analog);
|
|
wgtSetEnabled(sResetBtn, analog);
|
|
wgtSetEnabled(sZoneSldr, device >= 0);
|
|
wgtSliderSetValue(sZoneSldr, device >= 0 ? sJoy.getDeadZone(device) : 0);
|
|
showZone();
|
|
memset(&sShown, 0, sizeof(sShown));
|
|
|
|
for (int32_t i = 0; i < JOY_MAX_HATS; i++) {
|
|
sShown.hats[i] = JOY_HAT_CENTERED;
|
|
}
|
|
|
|
drawView();
|
|
}
|
|
|
|
|
|
static void showZone(void) {
|
|
char text[CPJ_KEY_BUF];
|
|
|
|
snprintf(text, sizeof(text), "%" PRId32 "%%", wgtSliderGetValue(sZoneSldr));
|
|
wgtSetText(sZoneLbl, text);
|
|
}
|
|
|
|
|
|
// Axis a of the controller being calibrated answers.
|
|
static bool wizAxis(int32_t axis) {
|
|
return (sWiz.info.axisPresent & (1U << axis)) != 0;
|
|
}
|