## Link to GitHub Issue or related Pull Request, if one exists Fixes #840 ## Description of change Currently, for subscreen games, mouse events are still delivered even when the subscreen overlay window is not visible. Change this so that the subscreen overlay must be active and under the mouse cursor for the mouse-to-touch transformation to occur. Applies to both native and wintouchemu. ## Testing Needs to test everything again.. iidx: - [ ] full screen with overlay - [ ] single window with overlay - [ ] two window Test: mouse, poke, api, real touch screen sdvx: - [ ] full screen with overlay - [ ] windowed popn - [x] full screen with overlay - [x] window with overlay - [x] dedicated window gitadora - [x] single window with overlay - [x] dedicated sub window nostalgia - [x] fullscreen - [x] windowed test: poke wintouchemu - [ ] Do all of the above again with wintouchemu
1009 lines
31 KiB
C++
1009 lines
31 KiB
C++
|
|
// enable touch functions - set version to windows 7
|
|
// mingw otherwise doesn't load touch stuff
|
|
#define _WIN32_WINNT 0x0601
|
|
|
|
#include <thread>
|
|
#include <mutex>
|
|
#include <windowsx.h>
|
|
|
|
#include "touch.h"
|
|
|
|
#include "avs/game.h"
|
|
#include "external/imgui/imgui.h"
|
|
#include "games/jb/jb.h"
|
|
#include "hooks/graphics/graphics.h"
|
|
#include "misc/eamuse.h"
|
|
#include "overlay/overlay.h"
|
|
#include "rawinput/touch.h"
|
|
#include "util/circular_buffer.h"
|
|
#include "util/detour.h"
|
|
#include "util/libutils.h"
|
|
#include "util/logging.h"
|
|
#include "util/time.h"
|
|
#include "util/utils.h"
|
|
|
|
#include "gdi_overlay.h"
|
|
#include "handler.h"
|
|
#include "touch_gestures.h"
|
|
#include "win7.h"
|
|
#include "win8.h"
|
|
|
|
// constants
|
|
const char *SPICETOUCH_CLASS_NAME = "spiceTouchClass";
|
|
const char *SPICETOUCH_FONT_NAME = "Courier New";
|
|
const char *INSERT_CARD_TEXT = "Insert Card";
|
|
|
|
// settings
|
|
static const int TOUCH_EVENT_BUFFER_SIZE = 1024 * 4;
|
|
static const int TOUCH_EVENT_BUFFER_THRESHOLD1 = 1024 * 2;
|
|
static const int TOUCH_EVENT_BUFFER_THRESHOLD2 = 1024 * 3;
|
|
|
|
// timer id for the overlay repaint tick
|
|
static const UINT_PTR SPICETOUCH_OVERLAY_TIMER_ID = 1;
|
|
|
|
// overlay repaint interval; the WinXP-compat build stays at 30 FPS
|
|
#if !SPICE_XP
|
|
static const int SPICETOUCH_OVERLAY_TIMER_MS = 1000 / 60;
|
|
#else
|
|
static const int SPICETOUCH_OVERLAY_TIMER_MS = 1000 / 30;
|
|
#endif // !SPICE_XP
|
|
|
|
// in mainline spicetools, this was false (show by default)
|
|
// in spice2x, this is true (hide by default)
|
|
bool SPICETOUCH_CARD_DISABLE = true;
|
|
HWND SPICETOUCH_TOUCH_HWND = nullptr;
|
|
int SPICETOUCH_TOUCH_X = 0;
|
|
int SPICETOUCH_TOUCH_Y = 0;
|
|
int SPICETOUCH_TOUCH_WIDTH = 0;
|
|
int SPICETOUCH_TOUCH_HEIGHT = 0;
|
|
|
|
// touch states
|
|
std::vector<TouchPoint> TOUCH_POINTS;
|
|
std::mutex TOUCH_POINTS_M;
|
|
static circular_buffer<TouchEvent> TOUCH_EVENTS(TOUCH_EVENT_BUFFER_SIZE);
|
|
std::mutex TOUCH_EVENTS_M;
|
|
|
|
// general states
|
|
static bool SPICETOUCH_INITIALIZED = false;
|
|
static bool SPICETOUCH_ATTACHED = false;
|
|
static bool SPICETOUCH_ATTACHED_DXHOOK = false;
|
|
static HWND SPICETOUCH_HWND = nullptr;
|
|
static WNDPROC SPICETOUCH_OLD_PROC = nullptr;
|
|
static bool SPICETOUCH_ENABLE_MOUSE = true;
|
|
static bool SPICETOUCH_REGISTERED_TOUCH = false;
|
|
static bool SPICETOUCH_CALL_OLD_PROC = false;
|
|
static std::thread *SPICETOUCH_TOUCH_THREAD = nullptr;
|
|
static HFONT SPICETOUCH_FONT;
|
|
static RECT SPICETOUCH_CARD_RECT;
|
|
static bool SPICETOUCH_CARD_ENABLED = false;
|
|
static const char *LOG_MODULE_NAME = "touch";
|
|
|
|
static TouchHandler *TOUCH_HANDLER = nullptr;
|
|
|
|
TouchHandler::TouchHandler(std::string name) {
|
|
log_info("touch", "Using touch handler: {}", name);
|
|
}
|
|
|
|
class RawInputTouchHandler : public TouchHandler {
|
|
public:
|
|
RawInputTouchHandler() : TouchHandler("rawinput") {
|
|
}
|
|
virtual bool window_register(HWND hWnd) override {
|
|
return true;
|
|
}
|
|
virtual bool window_unregister(HWND hWnd) override {
|
|
return true;
|
|
}
|
|
virtual void handle_message(msg_handler_result&, HWND, UINT, WPARAM, LPARAM) override {
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Add touch event but take care of buffer size
|
|
* Be careful, this doesn't lock the mutex on it's own
|
|
*/
|
|
void add_touch_event(TouchEvent *te) {
|
|
|
|
// check if first threshold is passed
|
|
if (TOUCH_EVENTS.size() > TOUCH_EVENT_BUFFER_THRESHOLD1) {
|
|
switch (te->type) {
|
|
case TOUCH_DOWN:
|
|
|
|
// ignore touch down events after first threshold
|
|
return;
|
|
|
|
case TOUCH_MOVE:
|
|
|
|
// add move event if we're not over the second threshold
|
|
if (TOUCH_EVENTS.size() <= TOUCH_EVENT_BUFFER_THRESHOLD2) {
|
|
TOUCH_EVENTS.put(*te);
|
|
}
|
|
|
|
return;
|
|
|
|
case TOUCH_UP:
|
|
|
|
// check if buffer is full
|
|
if (TOUCH_EVENTS.full()) {
|
|
|
|
// ignore event
|
|
return;
|
|
}
|
|
|
|
// when the buffer isn't full yet, add the touch up event
|
|
TOUCH_EVENTS.put(*te);
|
|
return;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
|
|
// add the touch up event
|
|
TOUCH_EVENTS.put(*te);
|
|
}
|
|
|
|
static void touch_initialize() {
|
|
|
|
// check if already initialized
|
|
if (SPICETOUCH_INITIALIZED) {
|
|
return;
|
|
}
|
|
SPICETOUCH_INITIALIZED = true;
|
|
|
|
if (!RI_MGR) {
|
|
log_fatal(
|
|
LOG_MODULE_NAME,
|
|
"touch_initialize() - RI Manager not available, called too early! "
|
|
"This is a BUG in spice, please file a bug report with log.txt.");
|
|
}
|
|
|
|
// initialize handler
|
|
if (rawinput::touch::is_enabled(RI_MGR.get())) {
|
|
TOUCH_HANDLER = new RawInputTouchHandler();
|
|
} else if (Win8Handler::is_available()) {
|
|
TOUCH_HANDLER = new Win8Handler();
|
|
} else if (Win7Handler::is_available()) {
|
|
TOUCH_HANDLER = new Win7Handler();
|
|
} else {
|
|
log_warning(LOG_MODULE_NAME, "no touch handler available");
|
|
}
|
|
}
|
|
|
|
static inline void touch_register_window(HWND hWnd) {
|
|
|
|
// register touch handling for window
|
|
if (TOUCH_HANDLER != nullptr) {
|
|
TOUCH_HANDLER->window_register(hWnd);
|
|
}
|
|
}
|
|
|
|
static inline void touch_unregister_window(HWND hWnd) {
|
|
|
|
// unregister touch handling for window
|
|
if (TOUCH_HANDLER != nullptr) {
|
|
TOUCH_HANDLER->window_unregister(hWnd);
|
|
}
|
|
}
|
|
|
|
bool is_touch_available(LPCSTR caller) {
|
|
static bool called_once = false;
|
|
|
|
if (!RI_MGR) {
|
|
log_fatal(
|
|
LOG_MODULE_NAME,
|
|
"is_touch_available({}) - RI Manager not available, called too early! "
|
|
"This is a BUG in spice, please file a bug report with log.txt.",
|
|
caller);
|
|
}
|
|
|
|
// initialize touch handler
|
|
touch_initialize();
|
|
|
|
if (!called_once) {
|
|
log_misc("touch", "is_touch_available called by: {}, returning {}",
|
|
caller, (TOUCH_HANDLER != nullptr) ? "TRUE" : "false");
|
|
called_once = true;
|
|
}
|
|
|
|
return TOUCH_HANDLER != nullptr;
|
|
}
|
|
|
|
void update_card_button() {
|
|
|
|
// check if enabled
|
|
if (!SPICETOUCH_CARD_ENABLED) {
|
|
return;
|
|
}
|
|
|
|
// check touch points
|
|
for (TouchPoint touchPoint : TOUCH_POINTS) {
|
|
POINT pt {};
|
|
pt.x = touchPoint.x;
|
|
pt.y = touchPoint.y;
|
|
|
|
if (PtInRect(&SPICETOUCH_CARD_RECT, pt) != 0) {
|
|
eamuse_card_insert(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void release_all_mouse_touch_points() {
|
|
std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
|
|
std::lock_guard<std::mutex> lock_events(TOUCH_EVENTS_M);
|
|
|
|
for (size_t x = 0; x < TOUCH_POINTS.size();) {
|
|
TouchPoint *tp = &TOUCH_POINTS[x];
|
|
|
|
if (tp->id == 0u) {
|
|
TouchEvent te {
|
|
.id = tp->id,
|
|
.x = tp->x,
|
|
.y = tp->y,
|
|
.type = TOUCH_UP,
|
|
.mouse = tp->mouse,
|
|
};
|
|
add_touch_event(&te);
|
|
|
|
TOUCH_POINTS.erase(TOUCH_POINTS.begin() + x);
|
|
} else {
|
|
x++;
|
|
}
|
|
}
|
|
}
|
|
|
|
static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|
|
|
// check if touch was registered
|
|
if (!SPICETOUCH_REGISTERED_TOUCH) {
|
|
SPICETOUCH_REGISTERED_TOUCH = true;
|
|
|
|
// register the handler when a touch screen is present
|
|
if (is_touch_available("SpiceTouchWndProc")) {
|
|
TOUCH_HANDLER->window_register(hWnd);
|
|
}
|
|
|
|
// enable the card button whenever the option is set, even without a touch
|
|
// screen (mouse clicks are handled as touch input and can trigger it)
|
|
if (!SPICETOUCH_CARD_DISABLE) {
|
|
SPICETOUCH_CARD_ENABLED = true;
|
|
}
|
|
}
|
|
|
|
// update rawinput touch display resolution
|
|
if (msg == WM_DISPLAYCHANGE) {
|
|
log_info("touch", "detected display mode change");
|
|
rawinput::touch::display_update();
|
|
}
|
|
|
|
const auto is_windowed_sub =
|
|
(GRAPHICS_IIDX_WSUB && hWnd == TDJ_SUBSCREEN_WINDOW) ||
|
|
(hWnd == SDVX_SUBSCREEN_WINDOW) ||
|
|
(hWnd == POPN_SUBSCREEN_WINDOW) ||
|
|
(hWnd == GFDM_SUBSCREEN_WINDOW);
|
|
|
|
if (msg == WM_CLOSE) {
|
|
if (is_windowed_sub) {
|
|
log_misc("touch", "ignore WM_CLOSE for subscreen window");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// check messages for dedicated window
|
|
if (SPICETOUCH_TOUCH_THREAD != nullptr) {
|
|
|
|
// check if overlay is enabled
|
|
auto overlay_enabled = overlay::OVERLAY
|
|
&& overlay::OVERLAY->renderer == overlay::OverlayRenderer::SOFTWARE;
|
|
|
|
// we don't want mouse events to interfere
|
|
SPICETOUCH_ENABLE_MOUSE = !overlay_enabled || !overlay::OVERLAY->get_active();
|
|
|
|
switch (msg) {
|
|
case WM_MOUSEACTIVATE: {
|
|
// Set the main window as the active window to reactivate the imgui cursor
|
|
HWND parent = GetParent(hWnd);
|
|
if (GetActiveWindow() != parent) {
|
|
SetActiveWindow(parent);
|
|
}
|
|
return MA_NOACTIVATE;
|
|
}
|
|
case WM_SETCURSOR: {
|
|
|
|
// set cursor back to the overlay one
|
|
if (overlay_enabled && LOWORD(lParam) == HTCLIENT && overlay::OVERLAY->update_cursor()) {
|
|
//return true; TODO: can make cursor invisible?
|
|
}
|
|
break;
|
|
}
|
|
case WM_TIMER: {
|
|
|
|
// request a repaint; the frame is composed into an offscreen buffer, so no
|
|
// background erase is needed (bErase = FALSE avoids a transparent flash)
|
|
InvalidateRect(hWnd, NULL, FALSE);
|
|
break;
|
|
}
|
|
case WM_PAINT: {
|
|
|
|
// get window rect
|
|
HWND parent = GetParent(hWnd);
|
|
RECT windowRect {}, clientRect {};
|
|
GetWindowRect(parent, &windowRect);
|
|
GetClientRect(parent, &clientRect);
|
|
|
|
// adjust to client area
|
|
POINT p1 {.x = clientRect.left, .y = clientRect.top};
|
|
POINT p2 {.x = clientRect.right, .y = clientRect.bottom};
|
|
ClientToScreen(parent, &p1);
|
|
ClientToScreen(parent, &p2);
|
|
windowRect.left = p1.x;
|
|
windowRect.top = p1.y;
|
|
windowRect.right = p2.x;
|
|
windowRect.bottom = p2.y;
|
|
|
|
// check if rect needs to update
|
|
RECT windowRectOld {};
|
|
GetWindowRect(hWnd, &windowRectOld);
|
|
if (memcmp(&windowRectOld, &windowRect, sizeof(RECT)) != 0) {
|
|
SetWindowPos(hWnd, HWND_TOP,
|
|
windowRect.left, windowRect.top,
|
|
windowRect.right - windowRect.left,
|
|
windowRect.bottom - windowRect.top,
|
|
SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE);
|
|
}
|
|
|
|
// render the software overlay before BeginPaint so only GDI composition
|
|
// and the final blit happen while the window is being painted
|
|
int overlay_width = 0, overlay_height = 0;
|
|
uint32_t *overlay_pixels = nullptr;
|
|
bool overlay_pixels_dirty = false;
|
|
if (overlay_enabled) {
|
|
overlay::OVERLAY->update();
|
|
overlay::OVERLAY->new_frame();
|
|
overlay::OVERLAY->render();
|
|
|
|
overlay_pixels = overlay::OVERLAY.get()->sw_get_pixel_data(
|
|
&overlay_width, &overlay_height);
|
|
overlay_pixels_dirty = overlay::OVERLAY->sw_pixels_dirty;
|
|
}
|
|
bool overlay_active = overlay_enabled && overlay::OVERLAY->get_active();
|
|
|
|
// compose the whole frame into an offscreen back buffer and present it with a
|
|
// single blit; the window never shows a half-erased (transparent) surface
|
|
// mid-paint, which is what caused the occasional flicker at higher frame rates
|
|
PAINTSTRUCT paint {};
|
|
HDC hdc = BeginPaint(hWnd, &paint);
|
|
|
|
RECT bufferRect {};
|
|
GetClientRect(hWnd, &bufferRect);
|
|
int buffer_width = bufferRect.right - bufferRect.left;
|
|
int buffer_height = bufferRect.bottom - bufferRect.top;
|
|
|
|
HBRUSH color_key_brush =
|
|
(HBRUSH) GetClassLongPtr(hWnd, GCLP_HBRBACKGROUND);
|
|
HDC draw_dc = touch_gdi_overlay_begin_frame(
|
|
hdc,
|
|
color_key_brush,
|
|
buffer_width,
|
|
buffer_height,
|
|
overlay_pixels,
|
|
overlay_pixels_dirty,
|
|
overlay_width,
|
|
overlay_height);
|
|
if (draw_dc == nullptr) {
|
|
EndPaint(hWnd, &paint);
|
|
return 0;
|
|
}
|
|
|
|
// draw the insert-card button below the jubeat debug overlay; it is
|
|
// hidden while the overlay is active
|
|
if (SPICETOUCH_CARD_ENABLED && SPICETOUCH_FONT != nullptr && !overlay_active) {
|
|
|
|
// create brushes
|
|
HBRUSH brushBorder = CreateSolidBrush(RGB(0, 196, 0));
|
|
HBRUSH brushFill = CreateSolidBrush(RGB(255, 192, 203));
|
|
|
|
// get window rect
|
|
RECT windowRect {};
|
|
GetWindowRect(hWnd, &windowRect);
|
|
|
|
bool should_rotate = avs::game::is_model({ "J44", "K44" });
|
|
|
|
// create box rect
|
|
RECT boxRect {};
|
|
if (should_rotate) {
|
|
boxRect.left = windowRect.right - 75;
|
|
boxRect.top = 20;
|
|
boxRect.right = windowRect.right - 44;
|
|
boxRect.bottom = 151;
|
|
} else {
|
|
boxRect.left = 20;
|
|
boxRect.top = 44;
|
|
boxRect.right = 141;
|
|
boxRect.bottom = 75;
|
|
}
|
|
|
|
// save box rect for touch input
|
|
SPICETOUCH_CARD_RECT = boxRect;
|
|
|
|
// draw borders
|
|
FillRect(draw_dc, &boxRect, brushBorder);
|
|
|
|
// modify box rect
|
|
boxRect.left += 1;
|
|
boxRect.top += 1;
|
|
boxRect.right -= 1;
|
|
boxRect.bottom -= 1;
|
|
|
|
// fill box
|
|
FillRect(draw_dc, &boxRect, brushFill);
|
|
|
|
// modify box rect
|
|
if (should_rotate) {
|
|
boxRect.left += 5 + 20;
|
|
boxRect.top += 5;
|
|
} else {
|
|
boxRect.left += 5;
|
|
boxRect.top += 5;
|
|
}
|
|
|
|
// draw text
|
|
SelectObject(draw_dc, SPICETOUCH_FONT);
|
|
SetTextColor(draw_dc, RGB(0, 196, 0));
|
|
DrawText(draw_dc, INSERT_CARD_TEXT, -1, &boxRect, DT_LEFT | DT_BOTTOM | DT_NOCLIP);
|
|
|
|
// delete objects
|
|
DeleteObject(brushFill);
|
|
DeleteObject(brushBorder);
|
|
}
|
|
|
|
#if !SPICE_XP
|
|
// draw the jubeat debug overlay on top (hidden while the overlay is active)
|
|
if (overlay_enabled && !overlay_active && games::jb::touch_debug_overlay_enabled()) {
|
|
games::jb::touch_draw_debug_overlay(draw_dc);
|
|
}
|
|
#endif // !SPICE_XP
|
|
|
|
// present the composed frame in a single blit
|
|
touch_gdi_overlay_present(hdc);
|
|
|
|
EndPaint(hWnd, &paint);
|
|
return 0;
|
|
}
|
|
case WM_CREATE: {
|
|
|
|
// set to layered window
|
|
SetWindowLong(hWnd, GWL_EXSTYLE, GetWindowLong(hWnd, GWL_EXSTYLE) | WS_EX_LAYERED);
|
|
|
|
// set alpha value
|
|
SetLayeredWindowAttributes(hWnd, 0, 0xFFu, LWA_ALPHA);
|
|
SetLayeredWindowAttributes(hWnd, RGB(255, 192, 203), 0, LWA_COLORKEY);
|
|
|
|
// get window rect
|
|
RECT windowRect {};
|
|
GetWindowRect(hWnd, &windowRect);
|
|
|
|
bool should_rotate = avs::game::is_model({ "J44", "K44" });
|
|
auto rotation = should_rotate ? 2700 : 0;
|
|
|
|
// load font
|
|
SPICETOUCH_FONT = CreateFont(20, 0, rotation, rotation, FW_NORMAL, FALSE, FALSE, FALSE,
|
|
ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
|
|
NONANTIALIASED_QUALITY, DEFAULT_PITCH,
|
|
SPICETOUCH_FONT_NAME);
|
|
if (SPICETOUCH_FONT == nullptr) {
|
|
log_warning(LOG_MODULE_NAME, "font '{}' could not be loaded", SPICETOUCH_FONT_NAME);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
case WM_DESTROY: {
|
|
touch_gdi_overlay_release();
|
|
if (SPICETOUCH_FONT != nullptr) {
|
|
DeleteObject(SPICETOUCH_FONT);
|
|
SPICETOUCH_FONT = nullptr;
|
|
}
|
|
PostQuitMessage(0);
|
|
return 0;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
msg_handler_result result {
|
|
.action = ACTION_PASS,
|
|
.return_value = 0,
|
|
};
|
|
|
|
// check if imgui is handling this mouse event
|
|
if (is_windowed_sub) {
|
|
// do nothing, don't let imgui hijack clicks on the sub window
|
|
result.action = ACTION_PASS;
|
|
|
|
} else if (overlay::OVERLAY != nullptr && overlay::OVERLAY->get_active() && ImGui::GetIO().WantCaptureMouse) {
|
|
result.action = ACTION_RETURN_DEFAULT;
|
|
|
|
} else if (TOUCH_HANDLER != nullptr) {
|
|
// call touch handler
|
|
TOUCH_HANDLER->handle_message(result, hWnd, msg, wParam, lParam);
|
|
}
|
|
|
|
if (result.action == ACTION_RETURN_STORED) {
|
|
|
|
// return the value from the touch handler
|
|
return result.return_value;
|
|
}
|
|
|
|
if (result.action == ACTION_PASS) {
|
|
|
|
// parse mouse messages
|
|
switch (msg) {
|
|
case WM_LBUTTONDOWN: {
|
|
// check if mouse is enabled
|
|
if (SPICETOUCH_ENABLE_MOUSE) {
|
|
if (is_mouse_message_from_touchscreen()) {
|
|
return 0;
|
|
}
|
|
|
|
// subscribe to mouse messages even when the cursor leaves the window
|
|
SetCapture(hWnd);
|
|
|
|
// release all old events before inserting a new one
|
|
release_all_mouse_touch_points();
|
|
|
|
// lock touch points
|
|
std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
|
|
std::lock_guard<std::mutex> lock_events(TOUCH_EVENTS_M);
|
|
|
|
// create touch point
|
|
TouchPoint tp {
|
|
.id = 0,
|
|
.x = GET_X_LPARAM(lParam),
|
|
.y = GET_Y_LPARAM(lParam),
|
|
.mouse = true,
|
|
.down_ms = get_performance_milliseconds(),
|
|
};
|
|
TOUCH_POINTS.push_back(tp);
|
|
|
|
// add touch down event
|
|
TouchEvent te {
|
|
.id = tp.id,
|
|
.x = tp.x,
|
|
.y = tp.y,
|
|
.type = TOUCH_DOWN,
|
|
.mouse = tp.mouse,
|
|
};
|
|
add_touch_event(&te);
|
|
|
|
// card button
|
|
update_card_button();
|
|
}
|
|
|
|
break;
|
|
}
|
|
case WM_MOUSEMOVE: {
|
|
// check if mouse is enabled
|
|
if (SPICETOUCH_ENABLE_MOUSE) {
|
|
if (is_mouse_message_from_touchscreen()) {
|
|
return 0;
|
|
}
|
|
|
|
// lock touch points
|
|
std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
|
|
std::lock_guard<std::mutex> lock_events(TOUCH_EVENTS_M);
|
|
|
|
// update point
|
|
for (auto &x : TOUCH_POINTS) {
|
|
TouchPoint *tp = &x;
|
|
|
|
// find ID 0
|
|
if (tp->id == 0u) {
|
|
|
|
// update touch point position
|
|
tp->x = GET_X_LPARAM(lParam);
|
|
tp->y = GET_Y_LPARAM(lParam);
|
|
|
|
// add touch move event
|
|
TouchEvent te {
|
|
.id = tp->id,
|
|
.x = tp->x,
|
|
.y = tp->y,
|
|
.type = TOUCH_MOVE,
|
|
.mouse = tp->mouse,
|
|
};
|
|
add_touch_event(&te);
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case WM_LBUTTONUP: {
|
|
// check if mouse is enabled
|
|
if (SPICETOUCH_ENABLE_MOUSE) {
|
|
if (is_mouse_message_from_touchscreen()) {
|
|
return 0;
|
|
}
|
|
|
|
release_all_mouse_touch_points();
|
|
if (GetCapture() == hWnd) {
|
|
ReleaseCapture();
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case WM_CAPTURECHANGED:
|
|
case WM_CANCELMODE: {
|
|
// to deal with window losing the capture after SetCapture
|
|
release_all_mouse_touch_points();
|
|
if (msg == WM_CANCELMODE && GetCapture() == hWnd) {
|
|
ReleaseCapture();
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
// call original function
|
|
if (SPICETOUCH_CALL_OLD_PROC && SPICETOUCH_OLD_PROC != nullptr) {
|
|
return SPICETOUCH_OLD_PROC(hWnd, msg, wParam, lParam);
|
|
}
|
|
}
|
|
}
|
|
|
|
// clean up
|
|
if (SPICETOUCH_ATTACHED_DXHOOK) {
|
|
return 0;
|
|
} else {
|
|
return DefWindowProc(hWnd, msg, wParam, lParam);
|
|
}
|
|
}
|
|
|
|
void touch_attach_wnd(HWND hWnd) {
|
|
|
|
// check if already attached
|
|
if (SPICETOUCH_ATTACHED) {
|
|
return;
|
|
}
|
|
SPICETOUCH_ATTACHED = true;
|
|
|
|
// initialize touch handler
|
|
touch_initialize();
|
|
touch_register_window(hWnd);
|
|
|
|
// hook window process
|
|
SPICETOUCH_HWND = hWnd;
|
|
SPICETOUCH_OLD_PROC = (WNDPROC) SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR) SpiceTouchWndProc);
|
|
|
|
// update rawinput
|
|
rawinput::touch::display_update();
|
|
}
|
|
|
|
void touch_attach_dx_hook() {
|
|
|
|
// check if already attached
|
|
if (SPICETOUCH_ATTACHED) {
|
|
return;
|
|
}
|
|
SPICETOUCH_ATTACHED = true;
|
|
SPICETOUCH_ATTACHED_DXHOOK = true;
|
|
|
|
// initialize touch handler
|
|
touch_initialize();
|
|
|
|
log_info("touch", "touch_attach_dx_hook: attaching SpiceTouchWndProc...");
|
|
|
|
// add dx hook
|
|
graphics_add_wnd_proc(SpiceTouchWndProc);
|
|
|
|
// update rawinput
|
|
rawinput::touch::display_update();
|
|
}
|
|
|
|
void touch_create_wnd(HWND hWnd, bool overlay) {
|
|
|
|
// check if already attached
|
|
if (SPICETOUCH_ATTACHED) {
|
|
return;
|
|
}
|
|
SPICETOUCH_ATTACHED = true;
|
|
|
|
// initialize touch handler
|
|
touch_initialize();
|
|
|
|
log_info("touch", "touch_create_wnd: creating SPICETOUCH_TOUCH_THREAD...");
|
|
|
|
// create thread
|
|
SPICETOUCH_TOUCH_THREAD = new std::thread([hWnd, overlay]() {
|
|
|
|
// create class
|
|
WNDCLASSEX wndClass {};
|
|
wndClass.cbSize = sizeof(WNDCLASSEX);
|
|
wndClass.style = 3;
|
|
wndClass.lpfnWndProc = SpiceTouchWndProc;
|
|
wndClass.cbClsExtra = 0;
|
|
wndClass.cbWndExtra = 0;
|
|
wndClass.hInstance = GetModuleHandle(nullptr);
|
|
wndClass.hIcon = nullptr;
|
|
wndClass.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
|
wndClass.hbrBackground = CreateSolidBrush(RGB(255, 192, 203));
|
|
wndClass.lpszMenuName = nullptr;
|
|
wndClass.lpszClassName = SPICETOUCH_CLASS_NAME;
|
|
wndClass.hIconSm = nullptr;
|
|
|
|
// register class
|
|
if (!RegisterClassExA(&wndClass)) {
|
|
log_warning(LOG_MODULE_NAME, "failed to register SpiceTouch class: {}", GetLastError());
|
|
return;
|
|
}
|
|
|
|
// calculate touch window dimensions
|
|
update_spicetouch_window_dimensions(hWnd);
|
|
|
|
// create window
|
|
HWND touch_window = CreateWindowExA(
|
|
0,
|
|
SPICETOUCH_CLASS_NAME,
|
|
"SpiceTools Touch",
|
|
(DWORD) CW_USEDEFAULT,
|
|
SPICETOUCH_TOUCH_X,
|
|
SPICETOUCH_TOUCH_Y,
|
|
SPICETOUCH_TOUCH_WIDTH,
|
|
SPICETOUCH_TOUCH_HEIGHT,
|
|
hWnd,
|
|
nullptr,
|
|
GetModuleHandle(nullptr),
|
|
nullptr
|
|
);
|
|
|
|
log_misc(
|
|
LOG_MODULE_NAME, "create SpiceTouch window ({}x{} @ {}, {})",
|
|
SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT, SPICETOUCH_TOUCH_X, SPICETOUCH_TOUCH_Y);
|
|
|
|
// check window
|
|
if (touch_window == nullptr) {
|
|
log_warning(LOG_MODULE_NAME, "failed to create SpiceTouch window: {}", GetLastError());
|
|
return;
|
|
}
|
|
|
|
// save reference
|
|
SPICETOUCH_TOUCH_HWND = touch_window;
|
|
|
|
// window settings
|
|
ShowWindow(touch_window, SW_SHOWNOACTIVATE);
|
|
UpdateWindow(touch_window);
|
|
|
|
// disable the OS touch contact visualization for our own touch window
|
|
disable_touch_gestures(touch_window);
|
|
|
|
// register
|
|
touch_register_window(touch_window);
|
|
|
|
// overlay
|
|
if (overlay && overlay::ENABLED) {
|
|
if (overlay::OVERLAY) {
|
|
log_warning(LOG_MODULE_NAME, "requested overlay, but already existing");
|
|
} else {
|
|
|
|
// create instance
|
|
overlay::OVERLAY.reset(new overlay::SpiceOverlay(touch_window));
|
|
|
|
// draw overlay repaint timer (30 FPS on WinXP, 60 FPS otherwise)
|
|
SetTimer(touch_window, SPICETOUCH_OVERLAY_TIMER_ID, SPICETOUCH_OVERLAY_TIMER_MS, NULL);
|
|
}
|
|
}
|
|
|
|
// window loop
|
|
MSG msg {};
|
|
while (GetMessage(&msg, nullptr, 0, 0) && SPICETOUCH_TOUCH_THREAD != nullptr) {
|
|
TranslateMessage(&msg);
|
|
DispatchMessage(&msg);
|
|
}
|
|
|
|
// kill overlay
|
|
if (overlay) {
|
|
overlay::OVERLAY.reset();
|
|
}
|
|
|
|
// unregister
|
|
touch_unregister_window(touch_window);
|
|
log_misc(LOG_MODULE_NAME, "window closed");
|
|
});
|
|
|
|
// update rawinput
|
|
rawinput::touch::display_update();
|
|
}
|
|
|
|
void touch_detach() {
|
|
|
|
// remove window process hook
|
|
if (SPICETOUCH_HWND != nullptr) {
|
|
touch_unregister_window(SPICETOUCH_HWND);
|
|
|
|
SetWindowLongPtr(SPICETOUCH_HWND, GWLP_WNDPROC, (LONG_PTR) SPICETOUCH_OLD_PROC);
|
|
SPICETOUCH_HWND = nullptr;
|
|
SPICETOUCH_OLD_PROC = nullptr;
|
|
}
|
|
|
|
// remove dx hook
|
|
graphics_remove_wnd_proc(SpiceTouchWndProc);
|
|
SPICETOUCH_TOUCH_THREAD = nullptr;
|
|
SPICETOUCH_ATTACHED_DXHOOK = false;
|
|
SPICETOUCH_ATTACHED = false;
|
|
}
|
|
|
|
void touch_write_points(std::vector<TouchPoint> *touch_points) {
|
|
|
|
// check size first
|
|
if (touch_points->empty()) {
|
|
return;
|
|
}
|
|
|
|
// lock
|
|
std::lock_guard<std::mutex> points(TOUCH_POINTS_M);
|
|
std::lock_guard<std::mutex> events(TOUCH_EVENTS_M);
|
|
|
|
// iterate through all the provided touch points
|
|
for (auto &tp : *touch_points) {
|
|
|
|
// find touch point to update
|
|
bool found = false;
|
|
for (auto &arr_tp : TOUCH_POINTS) {
|
|
if (arr_tp.id == tp.id) {
|
|
|
|
// mark as found
|
|
found = true;
|
|
|
|
// update position
|
|
arr_tp.x = tp.x;
|
|
arr_tp.y = tp.y;
|
|
|
|
// add touch move event
|
|
TouchEvent te {
|
|
.id = tp.id,
|
|
.x = tp.x,
|
|
.y = tp.y,
|
|
.type = TOUCH_MOVE,
|
|
.mouse = tp.mouse,
|
|
};
|
|
add_touch_event(&te);
|
|
}
|
|
}
|
|
|
|
// create new touch point when not found
|
|
if (!found) {
|
|
|
|
// stamp the landing time so debounce can measure the contact's age
|
|
tp.down_ms = get_performance_milliseconds();
|
|
|
|
// add touch point
|
|
TOUCH_POINTS.push_back(tp);
|
|
|
|
// add touch down event
|
|
TouchEvent te {
|
|
.id = tp.id,
|
|
.x = tp.x,
|
|
.y = tp.y,
|
|
.type = TOUCH_DOWN,
|
|
.mouse = tp.mouse,
|
|
};
|
|
add_touch_event(&te);
|
|
}
|
|
}
|
|
}
|
|
|
|
void touch_remove_points(std::vector<DWORD> *touch_point_ids) {
|
|
|
|
// check size first
|
|
if (touch_point_ids->empty()) {
|
|
return;
|
|
}
|
|
|
|
// lock
|
|
std::lock_guard<std::mutex> points(TOUCH_POINTS_M);
|
|
std::lock_guard<std::mutex> events(TOUCH_EVENTS_M);
|
|
|
|
// find the touch points to remove
|
|
for (auto id : *touch_point_ids) {
|
|
for (size_t x = 0; x < TOUCH_POINTS.size(); x++) {
|
|
TouchPoint *tp = &TOUCH_POINTS[x];
|
|
|
|
// check if the IDs match
|
|
if (tp->id == id) {
|
|
|
|
// add touch up event
|
|
TouchEvent te {
|
|
.id = id,
|
|
.x = tp->x,
|
|
.y = tp->y,
|
|
.type = TOUCH_UP,
|
|
.mouse = tp->mouse,
|
|
};
|
|
add_touch_event(&te);
|
|
|
|
// delete touch point
|
|
TOUCH_POINTS.erase(TOUCH_POINTS.begin() + x);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void touch_get_points(std::vector<TouchPoint> &touch_points, bool overlay) {
|
|
|
|
// update timeouts
|
|
if (RI_MGR) {
|
|
rawinput::touch::update_timeouts(RI_MGR.get());
|
|
}
|
|
|
|
// overlay override
|
|
if (!overlay &&
|
|
overlay::OVERLAY &&
|
|
overlay::OVERLAY->get_active() &&
|
|
!overlay::OVERLAY->has_subscreen_touch_transform() &&
|
|
ImGui::GetIO().WantCaptureMouse) {
|
|
|
|
return;
|
|
}
|
|
|
|
// lock
|
|
std::lock_guard<std::mutex> lock(TOUCH_POINTS_M);
|
|
|
|
// append touch points
|
|
touch_points.insert(touch_points.end(), TOUCH_POINTS.begin(), TOUCH_POINTS.end());
|
|
}
|
|
|
|
void touch_get_events(std::vector<TouchEvent> &touch_events, bool overlay) {
|
|
|
|
// update timeouts
|
|
if (RI_MGR) {
|
|
rawinput::touch::update_timeouts(RI_MGR.get());
|
|
}
|
|
|
|
// lock
|
|
std::lock_guard<std::mutex> lock(TOUCH_EVENTS_M);
|
|
|
|
// overlay override
|
|
if (!overlay &&
|
|
overlay::OVERLAY &&
|
|
overlay::OVERLAY->get_active() &&
|
|
!overlay::OVERLAY->has_subscreen_touch_transform() &&
|
|
ImGui::GetIO().WantCaptureMouse) {
|
|
|
|
TOUCH_EVENTS.reset();
|
|
return;
|
|
}
|
|
|
|
// append touch points
|
|
while (!TOUCH_EVENTS.empty()) {
|
|
touch_events.push_back(TOUCH_EVENTS.get());
|
|
}
|
|
}
|
|
|
|
void update_spicetouch_window_dimensions(HWND hWnd) {
|
|
RECT clientRect {};
|
|
GetClientRect(hWnd, &clientRect);
|
|
|
|
// adjust to client area
|
|
POINT topleft {.x = 0, .y = 0};
|
|
ClientToScreen(hWnd, &topleft);
|
|
POINT bottomright {.x = clientRect.right, .y = clientRect.bottom};
|
|
ClientToScreen(hWnd, &bottomright);
|
|
|
|
SPICETOUCH_TOUCH_X = topleft.x;
|
|
SPICETOUCH_TOUCH_Y = topleft.y;
|
|
SPICETOUCH_TOUCH_WIDTH = bottomright.x - topleft.x;
|
|
SPICETOUCH_TOUCH_HEIGHT = bottomright.y - topleft.y;
|
|
}
|
|
|
|
// https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
|
|
bool is_mouse_message_from_touchscreen() {
|
|
constexpr ULONG_PTR MI_WP_SIGNATURE = 0xFF515700;
|
|
constexpr ULONG_PTR SIGNATURE_MASK = 0xFFFFFF00;
|
|
return (GetMessageExtraInfo() & SIGNATURE_MASK) == MI_WP_SIGNATURE;
|
|
}
|