spicetools/rawinput/touch.cpp

568 lines
19 KiB
C++
Raw Normal View History

2024-08-28 15:10:34 +00:00
#include "touch.h"
#include <algorithm>
#include <windows.h>
#include <versionhelpers.h>
#include "util/logging.h"
#include "util/time.h"
#include "touch/touch.h"
// std::min
#ifdef min
#undef min
#endif
// std::max
#ifdef max
#undef max
#endif
namespace rawinput::touch {
// settings
bool DISABLED = false;
bool INVERTED = false;
// state
static bool DISPLAY_INITIALIZED = false;
static DWORD DISPLAY_ORIENTATION = DMDO_DEFAULT;
static long DISPLAY_SIZE_X = 1920L;
static long DISPLAY_SIZE_Y = 1080L;
bool is_touchscreen(Device *device) {
// check if disabled
if (DISABLED) {
return false;
}
// check device type
if (device->type != HID) {
return false;
}
auto *hid = device->hidInfo;
auto &attributes = hid->attributes;
// filter by VID/PID
if (attributes.VendorID > 0) {
// P2418HT
// touch points apparently aren't released and will stay
if (attributes.VendorID == 0x1FD2 && attributes.ProductID == 0x6103) {
return false;
}
}
// ignore the "Touch Pad" (0x05) usage under the "Digitizers" (0x0d) usage page
if (hid->caps.UsagePage == 0x0d && hid->caps.Usage == 0x05) {
return false;
}
// check description
if (device->desc == "HID-compliant touch screen") {
// TODO: this only works on english OS (?)
return true;
}
// we can also check if there's touch point values inside
for (const auto &value_name : hid->value_caps_names) {
if (value_name == "Contact identifier") {
return true;
}
}
// nope this one probably not
return false;
}
void enable(Device *device) {
if (device->type == HID) {
device->hidInfo->touch.valid = true;
log_info("rawinput", "enabled touchscreen device: {} ({})", device->desc, device->name);
} else {
log_fatal("rawinput", "tried to enable touch functionality on non HID device");
}
}
void disable(Device *device) {
if (device->type == HID) {
device->hidInfo->touch.valid = false;
log_info("rawinput", "disabled touchscreen device: {} ({})", device->desc, device->name);
} else
log_fatal("rawinput", "tried to disable touch functionality on non HID device");
}
void update_input(Device *device) {
// check type
if (device->type != HID) {
log_fatal("rawinput", "touch update called on non HID device");
return;
}
// get touch info
auto hid = device->hidInfo;
auto &touch = hid->touch;
if (!touch.valid) {
// not a registered touchscreen
return;
}
// parse elements
if (!touch.parsed_elements) {
log_info("rawinput", "parsing touch elements");
// clear lists first
touch.elements_contact_count.clear();
touch.elements_contact_identifier.clear();
touch.elements_x.clear();
touch.elements_y.clear();
touch.elements_width.clear();
touch.elements_height.clear();
touch.elements_pressed.clear();
touch.elements_pressure.clear();
// get value indices
for (int i = 0; i < (int) hid->value_caps_names.size(); i++) {
auto &name = hid->value_caps_names[i];
if (name == "Contact count") {
touch.elements_contact_count.push_back(i);
} else if (name == "X") {
touch.elements_x.push_back(i);
} else if (name == "Y") {
touch.elements_y.push_back(i);
} else if (name == "Width") {
touch.elements_width.push_back(i);
} else if (name == "Height") {
touch.elements_height.push_back(i);
} else if (name == "Contact identifier") {
touch.elements_contact_identifier.push_back(i);
} else if (name == "Pressure") {
touch.elements_pressure.push_back(i);
}
}
// get button indices
for (int i = 0; i < (int) hid->button_caps_names.size(); i++) {
auto &name = hid->button_caps_names[i];
if (name == "Tip Switch") {
touch.elements_pressed.push_back(i);
}
}
// check sizes
auto touch_size = touch.elements_contact_identifier.size();
if (touch_size != touch.elements_x.size() ||
touch_size != touch.elements_y.size() ||
touch_size != touch.elements_pressed.size())
{
log_info("rawinput", "touch element size mismatch: {}:contacts, {}:x, {}:y, {}:pressed",
touch.elements_contact_identifier.size(),
touch.elements_x.size(),
touch.elements_y.size(),
touch.elements_pressed.size());
disable(device);
return;
}
// mark as parsed
touch.parsed_elements = true;
log_info("rawinput", "touch elements parsed: {} status fields", touch.elements_x.size());
}
// check if display is initialized
if (!DISPLAY_INITIALIZED) {
display_update();
}
// update timeouts here as well so events are in the right order
update_timeouts(device);
// determine the number of touches contained in this report, defaulting to the size of `elements_x`
size_t touch_report_count = touch.elements_x.size();
if (!touch.elements_contact_count.empty()) {
// support devices that have multiple "Contact count" fields, for some reason
size_t contact_count = 0;
for (auto &index : touch.elements_contact_count) {
contact_count = std::max(contact_count, (size_t) hid->value_states_raw[index]);
}
// hybrid mode devices will report a contact count of 0 for subsequent reports that are
// part of the same initial frame
if (contact_count > 0) {
if (contact_count > touch_report_count) {
touch.remaining_contact_count = contact_count - touch_report_count;
} else {
touch_report_count = contact_count;
touch.remaining_contact_count = 0;
}
} else if (touch.remaining_contact_count > 0) {
if (touch.remaining_contact_count > touch_report_count) {
touch.remaining_contact_count -= touch_report_count;
} else {
touch_report_count = touch.remaining_contact_count;
touch.remaining_contact_count = 0;
}
}
}
// iterate all input data and get touch points
std::vector<HIDTouchPoint> touch_points;
touch_points.reserve(touch.elements_x.size());
for (size_t i = 0; i < touch.elements_x.size(); i++) {
// build touch point
HIDTouchPoint hid_tp{};
auto pos_x = hid->value_states[touch.elements_x[i]];
auto pos_y = hid->value_states[touch.elements_y[i]];
switch (DISPLAY_ORIENTATION) {
case DMDO_DEFAULT:
hid_tp.x = pos_x;
hid_tp.y = pos_y;
break;
case DMDO_90:
hid_tp.x = 1.f - pos_y;
hid_tp.y = pos_x;
break;
case DMDO_180:
hid_tp.x = 1.f - pos_x;
hid_tp.y = 1.f - pos_y;
break;
case DMDO_270:
hid_tp.x = pos_y;
hid_tp.y = 1.f - pos_x;
break;
default:
break;
}
// optionally invert
if (INVERTED) {
hid_tp.x = 1.f - hid_tp.x;
hid_tp.y = 1.f - hid_tp.y;
}
// check if this touch point should be considered valid
//
// If "Contact count" reports there are no touch reports remaining and the X and Y
// coordinates of this touch point are zero, then this report element should be
// skipped.
if (touch_report_count == 0 && hid_tp.x == 0.f && hid_tp.y == 0.f) {
continue;
} else if (touch_report_count > 0) {
touch_report_count--;
}
// generate ID (hopefully unique)
hid_tp.id = (DWORD) hid->value_states_raw[touch.elements_contact_identifier[i]];
hid_tp.id += (DWORD) (0xFFFFFF + device->id * 512);
//std::string src = "(none)";
// check if tip switch is down
size_t index_pressed = touch.elements_pressed[i];
for (auto &button_states : hid->button_states) {
if (index_pressed < button_states.size()) {
hid_tp.down = button_states[index_pressed];
/*
if (hid_tp.down)
src = "pressed (index: " + to_string(touch.elements_pressed[i]) + ", state: " + to_string(button_states[index_pressed]) +")";
*/
break;
} else
index_pressed -= button_states.size();
}
// check width/height of touch point to see if pressed
float width = 0.f, height = 0.f;
if (!hid_tp.down && i < touch.elements_width.size() && i < touch.elements_height.size()) {
width = hid->value_states[touch.elements_width[i]];
height = hid->value_states[touch.elements_height[i]];
hid_tp.down = width > 0.f && height > 0.f;
/*
if (hid_tp.down)
src = "width_height (width index: " + to_string(touch.elements_width[i]) + ", height index: " + to_string(touch.elements_height[i]) + ")";
*/
}
// so last thing we can check is the pressure
if (!hid_tp.down && i < touch.elements_pressure.size()) {
auto pressure = hid->value_states[touch.elements_pressure[i]];
hid_tp.down = pressure > 0.f;
/*
if (hid_tp.down)
src = "pressure (index: " + to_string(touch.elements_pressure[i]) + ")";
*/
}
/*
log_info("rawinput",
"touch i: " + to_string(i) +
" (id: " + to_string(hid_tp.id) +
"), ci: " + to_string(hid->value_states_raw[touch.elements_contact_identifier[i]]) +
", x: " + to_string(pos_x) +
", y: " + to_string(pos_y) +
", width: " + to_string(width) +
", height: " + to_string(height) +
", down: " + to_string(hid_tp.down) +
", src: " + src);
*/
// add to touch points
touch_points.emplace_back(hid_tp);
}
// process touch points
std::vector<DWORD> touch_removes;
std::vector<TouchPoint> touch_writes;
std::vector<DWORD> touch_modifications;
for (auto &hid_tp : touch_points) {
// check if existing
auto existing = std::find_if(
touch.touch_points.begin(),
touch.touch_points.end(),
[hid_tp] (const HIDTouchPoint &x) {
return x.id == hid_tp.id;
});
/*
ssize_t ttl = -1;
int64_t last_report = -1;
if (existing != touch.touch_points.end()) {
ttl = existing->ttl;
last_report = existing->last_report;
}
log_info("rawinput",
"id: " + to_string(hid_tp.id) +
", existing: " + to_string(existing != touch.touch_points.end()) +
", ttl: " + to_string(ttl) +
", last_report: " + to_string(last_report) +
", down: " + to_string(hid_tp.down));
*/
// check if pressed
if (!hid_tp.down) {
// only remove if it exists
if (existing != touch.touch_points.end()) {
// remove all touch points with this ID
touch_removes.push_back(hid_tp.id);
touch.touch_points.erase(std::remove_if(
touch.touch_points.begin(),
touch.touch_points.end(),
[hid_tp] (const HIDTouchPoint &x) {
return x.id == hid_tp.id;
}), touch.touch_points.end());
}
} else {
// write touch point
TouchPoint tp {
.id = hid_tp.id,
.x = (long) (hid_tp.x * DISPLAY_SIZE_X),
.y = (long) (hid_tp.y * DISPLAY_SIZE_Y),
.mouse = false,
};
touch_writes.push_back(tp);
touch_modifications.push_back(hid_tp.id);
// check if existing
if (existing == touch.touch_points.end()) {
// add new touch point
touch.touch_points.push_back(hid_tp);
} else {
// update existing touch point
*existing = hid_tp;
}
}
}
// set TTL and last report time
if (!touch.elements_x.empty() && !touch_modifications.empty()) {
auto ttl = touch.touch_points.size() / touch.elements_x.size() + 1;
auto system_time_ms = get_system_milliseconds();
for (auto &hid_tp_id : touch_modifications) {
for (auto &hid_tp : touch.touch_points) {
if (hid_tp.id == hid_tp_id) {
hid_tp.ttl = ttl;
hid_tp.last_report = system_time_ms;
}
}
}
}
// remove dead touch points
auto ttl_it = touch.touch_points.begin();
while (ttl_it != touch.touch_points.end()) {
auto &tp = *ttl_it;
if (tp.ttl == 0) {
touch_removes.push_back(tp.id);
ttl_it = touch.touch_points.erase(ttl_it);
} else {
tp.ttl--;
ttl_it++;
}
}
#ifndef SPICETOOLS_SPICECFG_STANDALONE
// update touch module
touch_remove_points(&touch_removes);
touch_write_points(&touch_writes);
#endif
}
void update_timeouts(Device *device) {
// check type
if (device->type != HID) {
log_fatal("rawinput", "touch timeout update called on non HID device");
return;
}
// get touch info
auto hid = device->hidInfo;
auto &touch = hid->touch;
if (!touch.valid) {
return; // not a registered touchscreen
}
// calculate deadline - we allow the devices 50ms of not sending shit
auto deadline = get_system_milliseconds() - 50;
// check touch points
std::vector<DWORD> touch_removes;
auto touch_it = touch.touch_points.begin();
while (touch_it != touch.touch_points.end()) {
auto &hid_tp = *touch_it;
// see if it's behind the deadline
if (hid_tp.last_report < deadline) {
// oops it's gone
touch_removes.push_back(hid_tp.id);
touch_it = touch.touch_points.erase(touch_it);
} else {
// check the next device
touch_it++;
}
}
#ifndef SPICETOOLS_SPICECFG_STANDALONE
// remove from touch module
touch_remove_points(&touch_removes);
#endif
}
void update_timeouts(RawInputManager *manager) {
// iterate all devices
for (auto &device : manager->devices_get()) {
// check if it's a valid touchscreen
if (device.type == HID && device.hidInfo->touch.valid) {
// lock n' load
device.mutex->lock();
update_timeouts(&device);
device.mutex->unlock();
}
}
}
bool is_enabled(RawInputManager *manager) {
// check if disabled or manager is null
if (DISABLED || manager == nullptr)
return false;
// check if at least one device is marked as valid touchscreen
for (auto &device : manager->devices_get()) {
if (device.type == HID && device.hidInfo->touch.valid) {
return true;
}
}
// no valid touch screen found
return false;
}
void display_update() {
// check if disabled
if (DISABLED)
return;
// determine monitor size
static RECT display_rect;
GetWindowRect(GetDesktopWindow(), &display_rect);
DISPLAY_SIZE_X = display_rect.right - display_rect.left;
DISPLAY_SIZE_Y = display_rect.bottom - display_rect.top;
log_info("rawinput", "display size: {}x{}", (int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y);
// determine monitor orientation
DEVMODE display_mode{};
display_mode.dmSize = sizeof(DEVMODE);
if (!EnumDisplaySettingsEx(nullptr, ENUM_CURRENT_SETTINGS, &display_mode, EDS_RAWMODE)) {
log_info("rawinput", "failed to determine monitor mode");
} else if (display_mode.dmFields & DM_DISPLAYORIENTATION) {
DISPLAY_ORIENTATION = display_mode.dmDisplayOrientation;
switch (DISPLAY_ORIENTATION) {
case DMDO_DEFAULT:
log_info("rawinput", "display rotation: 0");
break;
case DMDO_90:
log_info("rawinput", "display rotation: 90");
break;
case DMDO_180:
log_info("rawinput", "display rotation: 180");
break;
case DMDO_270:
log_info("rawinput", "display rotation: 270");
break;
default:
break;
}
// another XP fix
if (!IsWindowsVistaOrGreater()) {
switch (DISPLAY_ORIENTATION) {
case DMDO_90:
DISPLAY_ORIENTATION = DMDO_180;
log_info("rawinput", "flipping to 180");
break;
case DMDO_270:
DISPLAY_ORIENTATION = DMDO_90;
log_info("rawinput", "flipping to 90");
break;
default:
break;
}
}
} else {
log_info("rawinput", "failed to determine monitor orientation");
}
// mark as initialized
DISPLAY_INITIALIZED = true;
}
}