Skip to content

Commit aff75ee

Browse files
stefanverleysenclaude
authored andcommitted
fix: client touch click on fullscreen apps after screen switch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 561cf70 commit aff75ee

File tree

7 files changed

+195
-74
lines changed

7 files changed

+195
-74
lines changed

src/lib/client/Client.cpp

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -246,15 +246,6 @@ void Client::enter(SInt32 xAbs, SInt32 yAbs, UInt32, KeyModifierMask mask, bool)
246246
m_screen->mouseMove(xAbs, yAbs);
247247
m_screen->enter(mask);
248248

249-
if (m_pendingTouchActivation) {
250-
m_pendingTouchActivation = false;
251-
ARCH->sleep(0.1);
252-
LOG((CLOG_DEBUG1 "touch: replaying click at %d,%d (cursor at %d,%d)",
253-
m_touchActivateX, m_touchActivateY, xAbs, yAbs));
254-
m_screen->activateWindowAt(m_touchActivateX, m_touchActivateY);
255-
m_screen->fakeTouchClick(m_touchActivateX, m_touchActivateY);
256-
}
257-
258249
if (m_sendFileThread) {
259250
StreamChunker::interruptFile();
260251
m_sendFileThread.reset(nullptr);
@@ -739,9 +730,6 @@ void Client::handleGrabInput(const Event &event, void *)
739730
IPrimaryScreen::MotionInfo *info = static_cast<IPrimaryScreen::MotionInfo *>(event.getData());
740731
if (m_server != NULL) {
741732
LOG((CLOG_DEBUG1 "requesting grab input at %d,%d", info->m_x, info->m_y));
742-
m_pendingTouchActivation = true;
743-
m_touchActivateX = info->m_x;
744-
m_touchActivateY = info->m_y;
745733
m_server->grabInput(info->m_x, info->m_y);
746734
}
747735
}

src/lib/client/Client.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,4 @@ class Client : public IClient, public INode
260260
size_t m_maximumClipboardSize;
261261
deskflow::ClientArgs m_args;
262262
size_t m_resolvedAddressesCount = 0;
263-
bool m_pendingTouchActivation = false;
264-
SInt32 m_touchActivateX = 0;
265-
SInt32 m_touchActivateY = 0;
266263
};

src/lib/platform/MSWindowsDesks.cpp

Lines changed: 182 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ typedef LONG NTSTATUS;
9696
// x; y
9797
#define DESKFLOW_MSG_FAKE_TOUCH DESKFLOW_HOOK_LAST_MSG + 13
9898

99+
#define TOUCH_CLICK_TIMER_ID 1
100+
99101
static void send_keyboard_input(WORD wVk, WORD wScan, DWORD dwFlags)
100102
{
101103
INPUT inp;
@@ -451,6 +453,9 @@ LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM w
451453
SInt32 x = GET_X_LPARAM(lParam);
452454
SInt32 y = GET_Y_LPARAM(lParam);
453455
LOG((CLOG_DEBUG "secondary WM_POINTERDOWN touch at %d,%d", x, y));
456+
self->m_pendingTouchUp = true;
457+
self->m_pendingTouchX = x;
458+
self->m_pendingTouchY = y;
454459
PostThreadMessage(self->m_threadID, DESKFLOW_MSG_TOUCH,
455460
static_cast<WPARAM>(x), static_cast<LPARAM>(y));
456461
return 0;
@@ -459,6 +464,47 @@ LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM w
459464
break;
460465
}
461466

467+
case WM_POINTERUP: {
468+
MSWindowsDesks *self = reinterpret_cast<MSWindowsDesks *>(
469+
GetWindowLongPtr(hwnd, GWLP_USERDATA));
470+
if (self && self->m_pendingTouchUp) {
471+
SInt32 x = self->m_pendingTouchX;
472+
SInt32 y = self->m_pendingTouchY;
473+
if (self->m_isOnScreen) {
474+
self->m_pendingTouchUp = false;
475+
SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0,
476+
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW);
477+
ULONGLONG elapsed = GetTickCount64() - self->m_touchHideTime;
478+
LOG((CLOG_DEBUG "touch: finger up, hider hidden, %llu ms since shrink, injecting at %d,%d",
479+
elapsed, x, y));
480+
PostThreadMessage(GetCurrentThreadId(), DESKFLOW_MSG_FAKE_TOUCH,
481+
static_cast<WPARAM>(x), static_cast<LPARAM>(y));
482+
} else {
483+
self->m_touchLifted = true;
484+
LOG((CLOG_DEBUG "touch: finger up before enter, will fire on deskEnter at %d,%d", x, y));
485+
}
486+
}
487+
return 0;
488+
}
489+
490+
case WM_TIMER: {
491+
if (wParam == TOUCH_CLICK_TIMER_ID) {
492+
KillTimer(hwnd, TOUCH_CLICK_TIMER_ID);
493+
MSWindowsDesks *self = reinterpret_cast<MSWindowsDesks *>(
494+
GetWindowLongPtr(hwnd, GWLP_USERDATA));
495+
if (self) {
496+
ULONGLONG elapsed = GetTickCount64() - self->m_touchHideTime;
497+
LOG((CLOG_DEBUG "touch: timer fired, %llu ms after hide, injecting at %d,%d",
498+
elapsed, self->m_pendingTouchX, self->m_pendingTouchY));
499+
PostThreadMessage(GetCurrentThreadId(), DESKFLOW_MSG_FAKE_TOUCH,
500+
static_cast<WPARAM>(self->m_pendingTouchX),
501+
static_cast<LPARAM>(self->m_pendingTouchY));
502+
}
503+
return 0;
504+
}
505+
break;
506+
}
507+
462508
case WM_MOUSEMOVE: {
463509
LPARAM extraInfo = GetMessageExtraInfo();
464510
if ((extraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE) {
@@ -502,61 +548,112 @@ void MSWindowsDesks::deskMouseMove(SInt32 x, SInt32 y) const
502548

503549
void MSWindowsDesks::deskFakeTouchClick(SInt32 x, SInt32 y) const
504550
{
505-
static bool s_touchReady = false;
506-
507-
if (!s_touchReady) {
508-
if (!InitializeTouchInjection(1, TOUCH_FEEDBACK_DEFAULT)) {
509-
DWORD err = GetLastError();
510-
LOG((CLOG_WARN "touch: InitializeTouchInjection failed, err=%d", err));
511-
deskMouseMove(x, y);
512-
send_mouse_input(MOUSEEVENTF_LEFTDOWN, 0, 0, 0);
513-
send_mouse_input(MOUSEEVENTF_LEFTUP, 0, 0, 0);
514-
return;
515-
}
516-
LOG((CLOG_DEBUG1 "touch: InitializeTouchInjection succeeded"));
517-
s_touchReady = true;
551+
ULONGLONG elapsed = GetTickCount64() - m_touchHideTime;
552+
553+
POINT pt = {x, y};
554+
HWND target = WindowFromPoint(pt);
555+
HWND fg = GetForegroundWindow();
556+
557+
char targetClass[128] = {0};
558+
char targetTitle[128] = {0};
559+
char fgClass[128] = {0};
560+
char fgTitle[128] = {0};
561+
if (target) {
562+
GetClassNameA(target, targetClass, sizeof(targetClass));
563+
GetWindowTextA(target, targetTitle, sizeof(targetTitle));
564+
}
565+
if (fg) {
566+
GetClassNameA(fg, fgClass, sizeof(fgClass));
567+
GetWindowTextA(fg, fgTitle, sizeof(fgTitle));
518568
}
519569

520-
POINTER_TOUCH_INFO contact;
521-
memset(&contact, 0, sizeof(POINTER_TOUCH_INFO));
570+
LOG((CLOG_DEBUG "touch: injecting at %d,%d %llu ms after hide target=\"%s\" [%s] fg=\"%s\" [%s]",
571+
x, y, elapsed, targetTitle, targetClass, fgTitle, fgClass));
522572

523-
contact.pointerInfo.pointerType = PT_TOUCH;
524-
contact.pointerInfo.pointerId = 0;
525-
contact.pointerInfo.ptPixelLocation.x = x;
526-
contact.pointerInfo.ptPixelLocation.y = y;
573+
if (target) {
574+
HWND root = GetAncestor(target, GA_ROOT);
575+
if (root && root != fg) {
576+
DWORD targetThread = GetWindowThreadProcessId(root, NULL);
577+
DWORD curThread = GetCurrentThreadId();
527578

528-
contact.touchFlags = TOUCH_FLAG_NONE;
529-
contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE;
530-
contact.orientation = 90;
531-
contact.pressure = 32000;
532-
contact.rcContact.left = x - 2;
533-
contact.rcContact.right = x + 2;
534-
contact.rcContact.top = y - 2;
535-
contact.rcContact.bottom = y + 2;
579+
BOOL attached = FALSE;
580+
if (targetThread != 0 && targetThread != curThread) {
581+
attached = AttachThreadInput(targetThread, curThread, TRUE);
582+
}
536583

537-
contact.pointerInfo.pointerFlags =
538-
POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
584+
// bypass foreground lock
585+
mouse_event(MOUSEEVENTF_MOVE, 0, 0, 0, 0);
539586

540-
LOG((CLOG_DEBUG1 "touch: injecting DOWN at %d,%d", x, y));
587+
BOOL fgOk = SetForegroundWindow(root);
588+
BringWindowToTop(root);
541589

542-
bool injected = false;
543-
if (InjectTouchInput(1, &contact)) {
544-
Sleep(30);
545-
contact.pointerInfo.pointerFlags = POINTER_FLAG_UP;
546-
InjectTouchInput(1, &contact);
547-
injected = true;
548-
LOG((CLOG_DEBUG1 "touch: injected touch at %d,%d", x, y));
549-
} else {
550-
DWORD err = GetLastError();
551-
LOG((CLOG_WARN "touch: InjectTouchInput failed at %d,%d, err=%d", x, y, err));
590+
char rootTitle[128] = {0};
591+
GetWindowTextA(root, rootTitle, sizeof(rootTitle));
592+
593+
int waited = 0;
594+
while (GetForegroundWindow() != root && waited < 200) {
595+
Sleep(10);
596+
waited += 10;
597+
}
598+
HWND newFg = GetForegroundWindow();
599+
600+
if (attached) {
601+
AttachThreadInput(targetThread, curThread, FALSE);
602+
}
603+
604+
LOG((CLOG_DEBUG "touch: SetForegroundWindow(\"%s\")=%d, fg=0x%p (root=0x%p) waited=%dms",
605+
rootTitle, fgOk, newFg, root, waited));
606+
}
607+
}
608+
609+
static bool touchInitialized = false;
610+
if (!touchInitialized) {
611+
touchInitialized = InitializeTouchInjection(1, TOUCH_FEEDBACK_NONE) != 0;
612+
LOG((CLOG_DEBUG "touch: InitializeTouchInjection %s (err=%d)",
613+
touchInitialized ? "ok" : "FAILED", GetLastError()));
552614
}
553615

554-
Sleep(50);
616+
if (touchInitialized) {
617+
POINTER_TOUCH_INFO contact = {};
618+
contact.pointerInfo.pointerType = PT_TOUCH;
619+
contact.pointerInfo.pointerId = 0;
620+
contact.pointerInfo.ptPixelLocation.x = x;
621+
contact.pointerInfo.ptPixelLocation.y = y;
622+
contact.pointerInfo.pointerFlags =
623+
POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
624+
contact.touchFlags = TOUCH_FLAG_NONE;
625+
contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION |
626+
TOUCH_MASK_PRESSURE;
627+
contact.orientation = 90;
628+
contact.pressure = 32000;
629+
contact.rcContact.left = x - 2;
630+
contact.rcContact.right = x + 2;
631+
contact.rcContact.top = y - 2;
632+
contact.rcContact.bottom = y + 2;
633+
634+
BOOL downOk = InjectTouchInput(1, &contact);
635+
DWORD downErr = GetLastError();
636+
637+
Sleep(20);
638+
639+
contact.pointerInfo.pointerFlags =
640+
POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
641+
BOOL updOk = InjectTouchInput(1, &contact);
642+
643+
Sleep(20);
644+
645+
contact.pointerInfo.pointerFlags = POINTER_FLAG_UP;
646+
BOOL upOk = InjectTouchInput(1, &contact);
647+
DWORD upErr = GetLastError();
648+
649+
LOG((CLOG_DEBUG "touch: InjectTouchInput down=%d(err=%d) upd=%d up=%d(err=%d) at %d,%d",
650+
downOk, downErr, updOk, upOk, upErr, x, y));
651+
}
555652

556653
deskMouseMove(x, y);
557654
send_mouse_input(MOUSEEVENTF_LEFTDOWN, 0, 0, 0);
558655
send_mouse_input(MOUSEEVENTF_LEFTUP, 0, 0, 0);
559-
LOG((CLOG_DEBUG1 "touch: sent mouse click at %d,%d (touch injected=%d)", x, y, injected));
656+
LOG((CLOG_DEBUG "touch: SendInput mouse click at %d,%d", x, y));
560657
}
561658

562659
void MSWindowsDesks::deskMouseRelativeMove(SInt32 dx, SInt32 dy) const
@@ -634,31 +731,54 @@ void MSWindowsDesks::deskEnter(Desk *desk)
634731
ReleaseCapture();
635732

636733
LONG_PTR exStyle = GetWindowLongPtr(desk->m_window, GWL_EXSTYLE);
637-
// let hit-testing pass through to windows underneath
638734
SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle | WS_EX_TRANSPARENT);
639735
} else {
640736
registerTouchRawInput(desk->m_window, false);
641737
}
642738

643-
SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW);
739+
bool touchEnter = false;
740+
if (m_pendingTouchUp && m_touchLifted) {
741+
touchEnter = true;
742+
m_pendingTouchUp = false;
743+
m_touchLifted = false;
744+
SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0,
745+
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW);
746+
m_touchHideTime = GetTickCount64();
747+
SetTimer(desk->m_window, TOUCH_CLICK_TIMER_ID, 150, NULL);
748+
LOG((CLOG_DEBUG "touch: deskEnter path, hider hidden, timer started at %d,%d",
749+
m_pendingTouchX, m_pendingTouchY));
750+
} else if (m_pendingTouchUp) {
751+
touchEnter = true;
752+
SetWindowPos(desk->m_window, HWND_BOTTOM, m_xCenter, m_yCenter, 1, 1,
753+
SWP_NOACTIVATE);
754+
m_touchHideTime = GetTickCount64();
755+
LOG((CLOG_DEBUG "touch: deskEnter, finger still down, hider shrunk to 1x1"));
756+
} else {
757+
SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0,
758+
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW);
759+
}
644760

645761
setCursorVisibility(true);
646762

647763
HCURSOR arrow = LoadCursor(NULL, IDC_ARROW);
648764
SetClassLongPtr(desk->m_window, GCLP_HCURSOR, reinterpret_cast<LONG_PTR>(arrow));
649765
SetCursor(arrow);
650766

651-
// restore the foreground window
652-
// XXX -- this raises the window to the top of the Z-order. we
653-
// want it to stay wherever it was to properly support X-mouse
654-
// (mouse over activation) but i've no idea how to do that.
655-
// the obvious workaround of using SetWindowPos() to move it back
656-
// after being raised doesn't work.
657-
DWORD thisThread = GetWindowThreadProcessId(desk->m_window, NULL);
658-
DWORD thatThread = GetWindowThreadProcessId(desk->m_foregroundWindow, NULL);
659-
AttachThreadInput(thatThread, thisThread, TRUE);
660-
SetForegroundWindow(desk->m_foregroundWindow);
661-
AttachThreadInput(thatThread, thisThread, FALSE);
767+
if (touchEnter) {
768+
LOG((CLOG_DEBUG "touch: skipping foreground restore to preserve fullscreen app focus"));
769+
} else {
770+
// restore the foreground window
771+
// XXX -- this raises the window to the top of the Z-order. we
772+
// want it to stay wherever it was to properly support X-mouse
773+
// (mouse over activation) but i've no idea how to do that.
774+
// the obvious workaround of using SetWindowPos() to move it back
775+
// after being raised doesn't work.
776+
DWORD thisThread = GetWindowThreadProcessId(desk->m_window, NULL);
777+
DWORD thatThread = GetWindowThreadProcessId(desk->m_foregroundWindow, NULL);
778+
AttachThreadInput(thatThread, thisThread, TRUE);
779+
SetForegroundWindow(desk->m_foregroundWindow);
780+
AttachThreadInput(thatThread, thisThread, FALSE);
781+
}
662782
EnableWindow(desk->m_window, desk->m_lowLevel ? FALSE : TRUE);
663783
desk->m_foregroundWindow = NULL;
664784
}
@@ -864,8 +984,13 @@ bool MSWindowsDesks::parseHidTouch(const RAWINPUT *raw, const HidTouchDevice &de
864984
return false;
865985
}
866986

867-
outX = m_x + static_cast<SInt32>(rawX * m_w / dev.logicalMaxX);
868-
outY = m_y + static_cast<SInt32>(rawY * m_h / dev.logicalMaxY);
987+
// Touch digitizer maps to the primary monitor, not the virtual desktop
988+
SInt32 pw = GetSystemMetrics(SM_CXSCREEN);
989+
SInt32 ph = GetSystemMetrics(SM_CYSCREEN);
990+
outX = static_cast<SInt32>(rawX * pw / dev.logicalMaxX);
991+
outY = static_cast<SInt32>(rawY * ph / dev.logicalMaxY);
992+
LOG((CLOG_DEBUG1 "touch HID: raw=%lu,%lu logMax=%lu,%lu primary=%dx%d -> %d,%d",
993+
rawX, rawY, dev.logicalMaxX, dev.logicalMaxY, pw, ph, outX, outY));
869994
return true;
870995
}
871996

src/lib/platform/MSWindowsDesks.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,4 +316,10 @@ class MSWindowsDesks
316316
bool m_stopOnDeskSwitch;
317317

318318
std::unordered_map<HANDLE, HidTouchDevice> m_hidTouchDevices;
319+
320+
bool m_pendingTouchUp = false;
321+
bool m_touchLifted = false;
322+
SInt32 m_pendingTouchX = 0;
323+
SInt32 m_pendingTouchY = 0;
324+
ULONGLONG m_touchHideTime = 0;
319325
};

src/lib/server/PrimaryClient.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ void PrimaryClient::activateWindowAt(SInt32 x, SInt32 y)
7676
m_screen->activateWindowAt(x, y);
7777
}
7878

79+
void PrimaryClient::fakeTouchClick(SInt32 x, SInt32 y)
80+
{
81+
m_screen->fakeTouchClick(x, y);
82+
}
83+
7984
SInt32 PrimaryClient::getJumpZoneSize() const
8085
{
8186
return m_screen->getJumpZoneSize();

src/lib/server/PrimaryClient.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class PrimaryClient : public BaseClientProxy
8484
void fakeInputEnd();
8585

8686
void activateWindowAt(SInt32 x, SInt32 y);
87+
void fakeTouchClick(SInt32 x, SInt32 y);
8788

8889
//@}
8990
//! @name accessors

src/lib/server/Server.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,9 +1371,8 @@ void Server::handleTouchActivatedPrimaryEvent(const Event &event, void *)
13711371

13721372
switchScreen(m_primaryClient, x, y, false);
13731373

1374-
// The hook eats the original touch event, so the window under the
1375-
// touch point never receives it. Explicitly activate that window.
13761374
m_primaryClient->activateWindowAt(x, y);
1375+
m_primaryClient->fakeTouchClick(x, y);
13771376

13781377
m_touchSwitchCooldown.reset();
13791378
LOG((CLOG_DEBUG1 "touch switch cooldown started"));

0 commit comments

Comments
 (0)