462 lines
16 KiB
C++
Executable File
462 lines
16 KiB
C++
Executable File
void InputCaptureWindow::setup() {
|
|
window = new Window;
|
|
window->setObjectName("input-capture-window");
|
|
window->setWindowTitle("Input Capture");
|
|
|
|
layout = new QVBoxLayout;
|
|
layout->setMargin(Style::WindowMargin);
|
|
layout->setSpacing(0);
|
|
|
|
hlayout = new QHBoxLayout;
|
|
hlayout->setSpacing(Style::WidgetSpacing); {
|
|
title = new QLabel;
|
|
hlayout->addWidget(title, 0, Qt::AlignTop);
|
|
|
|
mouseAxes = new QPushButton(" Assign Mouse Axis ");
|
|
mouseAxes->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
hlayout->addWidget(mouseAxes, 0, Qt::AlignTop);
|
|
|
|
mouseButtons = new QPushButton(" Assign Mouse Button ");
|
|
mouseButtons->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
hlayout->addWidget(mouseButtons, 0, Qt::AlignTop);
|
|
}
|
|
layout->addLayout(hlayout);
|
|
|
|
imageSpacer = new QWidget;
|
|
imageSpacer->setFixedSize(Style::WidgetSpacing, Style::WidgetSpacing);
|
|
layout->addWidget(imageSpacer);
|
|
|
|
imageWidget = new ImageWidget;
|
|
layout->addWidget(imageWidget, 0, Qt::AlignHCenter);
|
|
|
|
spacer = new QWidget;
|
|
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
layout->addWidget(spacer);
|
|
|
|
window->setLayout(layout);
|
|
connect(mouseAxes, SIGNAL(released()), this, SLOT(assignMouseAxis()));
|
|
connect(mouseButtons, SIGNAL(released()), this, SLOT(assignMouseButton()));
|
|
|
|
winInputMouseCapture = new InputMouseCaptureWindow;
|
|
winInputMouseCapture->setup();
|
|
|
|
winInputCalibration = new InputCalibrationWindow;
|
|
winInputCalibration->setup();
|
|
}
|
|
|
|
void InputCaptureWindow::activate(InputObject *object) {
|
|
if(!activeGroup) window->hide();
|
|
|
|
utf8 info;
|
|
info << "<b>ID:</b> ";
|
|
if(object->parent) {
|
|
InputDevice *device = dynamic_cast<InputDevice*>(object->parent);
|
|
if(device) info << "Controller port " << (int)(device->port + 1) << " <b>::</b> ";
|
|
else info << "User interface <b>::</b> ";
|
|
info << object->parent->name << " <b>::</b> ";
|
|
}
|
|
info << object->name << "<br>";
|
|
|
|
activeObject = object;
|
|
if(activeObject->type == InputObject::Button) {
|
|
mouseAxes->hide();
|
|
mouseButtons->show();
|
|
|
|
info << "Press any key or button to assign to this ID.";
|
|
} else /*(activeObject->type == InputObject::Axis)*/ {
|
|
mouseAxes->show();
|
|
mouseButtons->hide();
|
|
|
|
info << "Move any axis to assign to this ID.";
|
|
}
|
|
|
|
if(dynamic_cast<Joypad*>(activeObject->parent)) {
|
|
imageSpacer->show();
|
|
imageWidget->setFixedSize(480, 210);
|
|
imageWidget->show();
|
|
} else {
|
|
imageSpacer->hide();
|
|
imageWidget->hide();
|
|
}
|
|
|
|
title->setText(info);
|
|
utility.showCentered(window);
|
|
}
|
|
|
|
void InputCaptureWindow::activate(InputGroup *group) {
|
|
activeGroup = group;
|
|
groupIndex = 0;
|
|
window->hide();
|
|
activate((*activeGroup)[groupIndex]);
|
|
}
|
|
|
|
void InputCaptureWindow::inputEvent(uint16_t code, bool forceAssign /* = false */) {
|
|
if(!activeObject || inputLock == true) return;
|
|
|
|
//input polling is global, need to block mouse actions that may be UI interactions.
|
|
//custom controls on window allow mouse assignment instead.
|
|
if(forceAssign == false) {
|
|
if(winInputMouseCapture->window->isActiveWindow()) {
|
|
winInputMouseCapture->inputEvent(code);
|
|
return;
|
|
}
|
|
if(!window->isActiveWindow()) return;
|
|
|
|
//get as much info as possible about this code
|
|
InputCode::type_t type = InputCode::type(code);
|
|
InputCode::axistype_t axisType = InputCode::axisType(code);
|
|
int joypadNumber = InputCode::joypadNumber(code);
|
|
int16_t state = inputManager.state(code);
|
|
unsigned distance = abs(state - inputManager.lastState(code));
|
|
|
|
if(type == InputCode::JoypadHat) {
|
|
//hats can be in any of nine clock-wise states (4x direct, 4x angled and centered.)
|
|
//only map when hat is moved to an explicit direction.
|
|
if(state != joypad<>::hat_up && state != joypad<>::hat_down
|
|
&& state != joypad<>::hat_left && state != joypad<>::hat_right) return;
|
|
}
|
|
|
|
if(type == InputCode::JoypadAxis) {
|
|
//require a degree of resistance to prevent accidental mapping
|
|
if(axisType == InputCode::Stick) {
|
|
//require axis to be pressed almost completely toward a specific direction
|
|
if(state > -28672 && state < +28672) return;
|
|
} else if(axisType == InputCode::Trigger) {
|
|
//require trigger to be at least 75% pressed down
|
|
if(state > -16384) return;
|
|
} else {
|
|
//invalid axis type: most likely the controller has yet to be calibrated
|
|
if(joypadNumber < 0) return; //should never occur
|
|
|
|
//some analog triggers report phantom motion even when user does not touch gamepad
|
|
if(distance < 64) return; //require some degree of force to trigger calibration
|
|
if(state > -8192 && state < +8192) return; //ignore center motion
|
|
|
|
if(inputManager.calibrated(joypadNumber) == false) {
|
|
winInputCalibration->activate(joypadNumber);
|
|
}
|
|
|
|
//block assignment until controller is fully calibrated
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(activeObject->type == InputObject::Axis) {
|
|
if(type == InputCode::KeyboardButton
|
|
|| type == InputCode::MouseAxis
|
|
|| type == InputCode::MouseButton
|
|
|| type == InputCode::JoypadHat
|
|
|| type == InputCode::JoypadButton
|
|
) return;
|
|
|
|
//uni-directional trigger cannot map to bi-directional axis
|
|
if(type == InputCode::JoypadAxis && axisType == InputCode::Trigger) return;
|
|
}
|
|
|
|
if(activeObject->type == InputObject::Button) {
|
|
if(type == InputCode::MouseAxis
|
|
|| type == InputCode::MouseButton
|
|
) return;
|
|
|
|
//only capture on button press, not release
|
|
if(type != InputCode::JoypadAxis && state == false) return;
|
|
}
|
|
|
|
//if assigning a complete controller set, ensure requested key has not been assigned
|
|
//to a previous entry in the group already. this prevents slow motion of joypad axes
|
|
//from assigning the same value to multiple entries in rapid succession.
|
|
if(activeGroup && groupIndex > 0) {
|
|
for(unsigned i = 0; i < groupIndex; i++) {
|
|
if(code == (*activeGroup)[i]->code) {
|
|
//joypad hats and axes have multiple states, and are differentiated by modifier.
|
|
//allow mapping only if requested modifier is unique.
|
|
if(type == InputCode::JoypadHat) {
|
|
if(state == joypad<>::hat_up && (*activeGroup)[i]->modifier == InputObject::Up ) return;
|
|
if(state == joypad<>::hat_down && (*activeGroup)[i]->modifier == InputObject::Down ) return;
|
|
if(state == joypad<>::hat_left && (*activeGroup)[i]->modifier == InputObject::Left ) return;
|
|
if(state == joypad<>::hat_right && (*activeGroup)[i]->modifier == InputObject::Right) return;
|
|
} else if(type == InputCode::JoypadAxis) {
|
|
if(axisType == InputCode::Stick) {
|
|
if(state < 0 && (*activeGroup)[i]->modifier == InputObject::Lo) return;
|
|
if(state >= 0 && (*activeGroup)[i]->modifier == InputObject::Hi) return;
|
|
} else if(axisType == InputCode::Trigger) {
|
|
if((*activeGroup)[i]->modifier == InputObject::Trigger) return;
|
|
}
|
|
} else {
|
|
//this code has already been used, do not map it
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//bind code and update GUI input assignment list
|
|
activeObject->bind(code);
|
|
winInputSettings->updateList();
|
|
activeObject = 0;
|
|
|
|
//ignore multiple simultaneous state changes.
|
|
//this helps with joypads that only activate
|
|
//analog inputs after the first button press.
|
|
inputLock = true;
|
|
for(unsigned i = 0; i < 2; i++) inputManager.refresh();
|
|
inputLock = false;
|
|
|
|
if(!activeGroup) {
|
|
window->hide();
|
|
winInputMouseCapture->window->hide();
|
|
} else {
|
|
//try and map the next code in this input group
|
|
groupIndex++;
|
|
if(groupIndex < activeGroup->size()) {
|
|
activate((*activeGroup)[groupIndex]);
|
|
} else {
|
|
//all group codes mapped
|
|
window->hide();
|
|
winInputMouseCapture->window->hide();
|
|
activeGroup = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
void InputCaptureWindow::assignMouseAxis() {
|
|
//refresh input state so that mouse release event (from SIGNAL(released())
|
|
//is not sent immediately after window is visible.
|
|
inputManager.refresh();
|
|
winInputMouseCapture->activate(InputMouseCaptureWindow::AxisMode);
|
|
}
|
|
|
|
void InputCaptureWindow::assignMouseButton() {
|
|
inputManager.refresh();
|
|
winInputMouseCapture->activate(InputMouseCaptureWindow::ButtonMode);
|
|
}
|
|
|
|
InputCaptureWindow::InputCaptureWindow() {
|
|
activeObject = 0;
|
|
activeGroup = 0;
|
|
groupIndex = 0;
|
|
inputLock = false;
|
|
}
|
|
|
|
void InputCaptureWindow::Window::closeEvent(QCloseEvent*) {
|
|
//window closed by user, cancel key assignment
|
|
winInputCapture->activeObject = 0;
|
|
winInputCapture->activeGroup = 0;
|
|
|
|
winInputMouseCapture->window->hide();
|
|
winInputCalibration->dismiss();
|
|
}
|
|
|
|
void InputCaptureWindow::ImageWidget::paintEvent(QPaintEvent*) {
|
|
//currently, there is only an image available for the joypad.
|
|
//in the future, this routine should determine which type of
|
|
//image to draw via activeObject->parent's derived class type.
|
|
QPainter painter(this);
|
|
QPixmap pixmap(":/joypad.png");
|
|
painter.drawPixmap(0, 0, pixmap);
|
|
}
|
|
|
|
//=======================
|
|
//InputMouseCaptureWindow
|
|
//=======================
|
|
|
|
void InputMouseCaptureWindow::setup() {
|
|
window = new QWidget;
|
|
window->setObjectName("input-mouse-capture-window");
|
|
window->setWindowTitle("Mouse Input Capture");
|
|
|
|
layout = new QVBoxLayout;
|
|
layout->setMargin(Style::WindowMargin);
|
|
layout->setSpacing(0);
|
|
|
|
info = new QLabel;
|
|
layout->addWidget(info);
|
|
layout->addSpacing(Style::WidgetSpacing);
|
|
|
|
captureBox = new QLabel("[ capture box ]");
|
|
captureBox->setObjectName("mouse-capture-box");
|
|
captureBox->setAlignment(Qt::AlignCenter);
|
|
captureBox->setFixedHeight(120);
|
|
layout->addWidget(captureBox);
|
|
|
|
buttonLayout = new QHBoxLayout;
|
|
buttonLayout->setSpacing(Style::WidgetSpacing); {
|
|
xAxis = new QPushButton("X-axis");
|
|
buttonLayout->addWidget(xAxis);
|
|
|
|
yAxis = new QPushButton("Y-axis");
|
|
buttonLayout->addWidget(yAxis);
|
|
}
|
|
layout->addLayout(buttonLayout);
|
|
|
|
spacer = new QWidget;
|
|
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
layout->addWidget(spacer);
|
|
|
|
window->setLayout(layout);
|
|
connect(xAxis, SIGNAL(released()), this, SLOT(assignAxisX()));
|
|
connect(yAxis, SIGNAL(released()), this, SLOT(assignAxisY()));
|
|
}
|
|
|
|
void InputMouseCaptureWindow::activate(InputMouseCaptureWindow::Mode mode_) {
|
|
window->hide();
|
|
activeMode = mode_;
|
|
|
|
if(activeMode == AxisMode) {
|
|
captureBox->hide();
|
|
xAxis->show();
|
|
yAxis->show();
|
|
|
|
info->setText(utf8()
|
|
<< "To assign a mouse axis to this ID, please click the desired axis button below,<br>"
|
|
<< "using the mouse that you want the axis to be assigned to."
|
|
);
|
|
|
|
activeMouse = -1;
|
|
} else /*(activeMode == ButtonMode) */ {
|
|
captureBox->show();
|
|
xAxis->hide();
|
|
yAxis->hide();
|
|
|
|
info->setText(utf8()
|
|
<< "To assign a mouse button to this ID, please place the mouse you wish to map<br>"
|
|
<< "over the capture box below, and then click any mouse button to set assignment."
|
|
);
|
|
}
|
|
|
|
utility.showCentered(window);
|
|
}
|
|
|
|
//this is only called when isActiveWindow() == true
|
|
void InputMouseCaptureWindow::inputEvent(uint16_t code) {
|
|
InputCode::type_t type = InputCode::type(code);
|
|
int16_t state = inputManager.state(code);
|
|
|
|
if(activeMode == AxisMode) {
|
|
//when pressing down mouse button (eg to select "X-axis" or "Y-axis"),
|
|
//record mouse index for later assignment
|
|
if(type == InputCode::MouseButton && state == true) {
|
|
activeMouse = InputCode::mouseNumber(code);
|
|
return;
|
|
}
|
|
} else if(activeMode == ButtonMode) {
|
|
//if this is a mouse button that is being released ...
|
|
if(type == InputCode::MouseButton && state == false) {
|
|
//ensure button was clicked inside active capture box
|
|
QRect windowRect = window->geometry();
|
|
QRect widgetRect = captureBox->geometry();
|
|
unsigned wx = windowRect.left() + widgetRect.left();
|
|
unsigned wy = windowRect.top() + widgetRect.top();
|
|
unsigned px = QCursor::pos().x();
|
|
unsigned py = QCursor::pos().y();
|
|
|
|
if(px < wx || px >= wx + widgetRect.size().width() ) return;
|
|
if(py < wy || py >= wy + widgetRect.size().height()) return;
|
|
|
|
winInputCapture->inputEvent(code, true);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void InputMouseCaptureWindow::assignAxisX() {
|
|
if(activeMouse >= 0) {
|
|
winInputCapture->inputEvent(mouse<>::index(activeMouse, mouse<>::x), true);
|
|
}
|
|
}
|
|
|
|
void InputMouseCaptureWindow::assignAxisY() {
|
|
if(activeMouse >= 0) {
|
|
winInputCapture->inputEvent(mouse<>::index(activeMouse, mouse<>::y), true);
|
|
}
|
|
}
|
|
|
|
//====================
|
|
//InputCalibrateWindow
|
|
//====================
|
|
|
|
//background:
|
|
//===========
|
|
//HID devices work by sending device state *changes* only. what this means is that when an application is started,
|
|
//it does not know the current state of said device. the keyboard and mouse are exceptions, as the OS globally
|
|
//tracks their states. but this does apply to joypads. once a button is pressed, or an axis is moved, the entire
|
|
//joypad state will be sent in a message, that APIs such as DirectInput and SDL will buffer.
|
|
//
|
|
//to complicate matters, recent joypads have added pressure-sensitive buttons (triggers), in addition to the
|
|
//existing analog sticks. but this functionality was not extended to the USB HID state or to various platform
|
|
//input APIs. instead, they are treated exactly like axes.
|
|
//
|
|
//however, an application needs to treat these two input types distinctly:
|
|
//a stick is a bi-directional input. the stick starts off centered, and can be moved left or right, or up or down.
|
|
//a trigger is a uni-directional input. it can only be pushed down.
|
|
//
|
|
//a stick's default, unpressed state is centered (0), whereas a trigger's default state is fully depressed (+32767.)
|
|
//but because the default state is not available until the user presses a button on a joypad, it is not possible to
|
|
//calibrate a joypad on startup. all axes will report a value of 0, even if buttons are depressed.
|
|
//
|
|
//thusly, this class is needed. it will spawn a window upon the first attempt to map a joypad axis after startup.
|
|
//by the point this window appears, an axis must have been moved, so the joypad state is now valid. but since it's
|
|
//not possible to tell which button was pressed or which axis was moved, it's possible the axis that we're trying to
|
|
//map was moved. so querying its state now might result in improper mapping. so instead, this window is shown, and
|
|
//the user is asked not to press any buttons or move any axes. after hitting okay to confirm the joypad is idle,
|
|
//the joypad can finally be calibrated properly.
|
|
//
|
|
//upon assignment, the calibration data is appended to the input assignment value (eg "joypad00.axis00::trigger"),
|
|
//so that calibration is not necessary every time the emulator is run -- only when modifying input mapping on an axis.
|
|
|
|
void InputCalibrationWindow::activate(unsigned joy) {
|
|
//do not override an already active calibration window
|
|
if(window->isVisible()) return;
|
|
|
|
activeJoypad = joy;
|
|
info->setText(utf8()
|
|
<< "Joypad #" << joy << " needs to be calibrated before it can be mapped. "
|
|
<< "Please ensure that<br>no buttons are pressed, "
|
|
<< "and all axes are centered before pressing okay."
|
|
);
|
|
|
|
utility.showCentered(window);
|
|
ok->setFocus();
|
|
}
|
|
|
|
void InputCalibrationWindow::setup() {
|
|
window = new Window;
|
|
window->setObjectName("input-calibrate-window");
|
|
window->setWindowTitle("Joypad Calibration");
|
|
|
|
layout = new QVBoxLayout;
|
|
layout->setMargin(Style::WindowMargin);
|
|
layout->setSpacing(0);
|
|
|
|
info = new QLabel;
|
|
layout->addWidget(info);
|
|
layout->addSpacing(Style::WidgetSpacing);
|
|
|
|
ok = new QPushButton("Ok");
|
|
layout->addWidget(ok);
|
|
|
|
spacer = new QWidget;
|
|
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
layout->addWidget(spacer);
|
|
|
|
window->setLayout(layout);
|
|
connect(ok, SIGNAL(released()), this, SLOT(dismiss()));
|
|
}
|
|
|
|
void InputCalibrationWindow::dismiss() {
|
|
window->hide();
|
|
if(activeJoypad != -1) {
|
|
inputManager.calibrate(activeJoypad);
|
|
activeJoypad = -1;
|
|
}
|
|
}
|
|
|
|
void InputCalibrationWindow::Window::closeEvent(QCloseEvent*) {
|
|
winInputCalibration->dismiss();
|
|
}
|
|
|
|
InputCalibrationWindow::InputCalibrationWindow() {
|
|
activeJoypad = -1;
|
|
}
|