From 196f8eb52787ae275dada9e7a665750f2a33819a Mon Sep 17 00:00:00 2001 From: Stefan Verleysen Date: Wed, 11 Feb 2026 10:53:09 -0500 Subject: [PATCH 01/25] =?UTF-8?q?feat:=20touch=20activates=20screen=20?= =?UTF-8?q?=E2=80=94=20switch=20to=20touched=20computer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a touchActivateScreen option that switches keyboard/mouse focus to whichever computer the user touches. Works bidirectionally between server and clients, and detects touch in all applications including Chrome, Electron, UWP, and legacy Win32 apps. Touch detection uses three independent paths for broad hardware coverage: low-level mouse hook (dwExtraInfo MI_WP_SIGNATURE), raw input (RIDEV_INPUTSINK on desk thread for WM_INPUT), and WM_POINTER messages (Win8+ API). All three converge through debounced event dispatch to the server's screen-switch logic. Raw input is registered on the desk thread window rather than the main window because the main event loop uses QS_ALLPOSTMESSAGE, which never wakes for WM_INPUT messages. Cursor hiding on touchscreen clients uses a full-screen hider window with a blank cursor class instead of ShowCursor(FALSE), which is unreliable on touch hardware. WS_EX_TRANSPARENT is toggled on leave/enter for correct hit-testing. The server synthesizes a click and forces the foreground window after touch-triggered switches so the window under the touch point receives focus. A 500ms cooldown prevents edge-triggered switches from immediately undoing touch switches. Tested on: - Server: ASUS 3090DEV, i9-12900K, RTX 3090, Windows 11 Enterprise Build 26100 (64-bit), USB HID touch screen (VID 0457 / PID 0819) - Client: Microsoft Surface Book (1st gen), integrated touchscreen, Windows 11 - Applications tested: Chrome, Cursor (Electron), Windows Settings (UWP), Notepad, Synergy GUI, Start menu, File Explorer --- src/gui/CMakeLists.txt | 2 + src/gui/src/ServerConfig.cpp | 10 +- src/gui/src/ServerConfig.h | 9 ++ src/gui/src/ServerConfigDialog.cpp | 9 ++ src/gui/src/ServerConfigDialogBase.ui | 11 ++ src/lib/base/EventTypes.cpp | 3 + src/lib/base/EventTypes.h | 34 ++++++- src/lib/client/Client.cpp | 16 +++ src/lib/client/Client.h | 1 + src/lib/client/ServerProxy.cpp | 6 ++ src/lib/client/ServerProxy.h | 7 ++ src/lib/deskflow/option_types.h | 1 + src/lib/deskflow/protocol_types.cpp | 1 + src/lib/deskflow/protocol_types.h | 5 + src/lib/platform/MSWindowsDesks.cpp | 101 ++++++++++++++----- src/lib/platform/MSWindowsHook.cpp | 36 +++++++ src/lib/platform/MSWindowsHook.h | 4 + src/lib/platform/MSWindowsScreen.cpp | 129 +++++++++++++++++++++++- src/lib/platform/MSWindowsScreen.h | 10 ++ src/lib/platform/dfwhook.h | 3 +- src/lib/server/ClientProxy1_0.cpp | 22 ++++ src/lib/server/ClientProxy1_0.h | 1 + src/lib/server/Config.cpp | 2 + src/lib/server/Server.cpp | 139 ++++++++++++++++++++++++++ src/lib/server/Server.h | 7 ++ 25 files changed, 535 insertions(+), 34 deletions(-) diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 0064323e2..b042bcad4 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -41,7 +41,9 @@ include_directories(./src) # gui library autogen headers: # qt doesn't seem to auto include the autogen headers for libraries. +# single-config generators (Make) use include/, multi-config (VS) use include_$/ include_directories(${PROJECT_BINARY_DIR}/src/lib/gui/gui_autogen/include) +include_directories(${PROJECT_BINARY_DIR}/src/lib/gui/gui_autogen/include_$) # generated includes include_directories(${PROJECT_BINARY_DIR}/config) diff --git a/src/gui/src/ServerConfig.cpp b/src/gui/src/ServerConfig.cpp index 39d2aa8e6..018ea70a4 100644 --- a/src/gui/src/ServerConfig.cpp +++ b/src/gui/src/ServerConfig.cpp @@ -80,7 +80,8 @@ bool ServerConfig::operator==(const ServerConfig &sc) const m_SwitchCornerSize == sc.m_SwitchCornerSize && m_SwitchCorners == sc.m_SwitchCorners && m_Hotkeys == sc.m_Hotkeys && m_pAppConfig == sc.m_pAppConfig && m_DisableLockToScreen == sc.m_DisableLockToScreen && m_ClipboardSharing == sc.m_ClipboardSharing && - m_ClipboardSharingSize == sc.m_ClipboardSharingSize && m_pMainWindow == sc.m_pMainWindow; + m_ClipboardSharingSize == sc.m_ClipboardSharingSize && m_TouchActivateScreen == sc.m_TouchActivateScreen && + m_pMainWindow == sc.m_pMainWindow; } void ServerConfig::save(QFile &file) const @@ -127,6 +128,7 @@ void ServerConfig::commit() settings().setValue("disableLockToScreen", disableLockToScreen()); settings().setValue("clipboardSharing", clipboardSharing()); settings().setValue("clipboardSharingSize", QVariant::fromValue(clipboardSharingSize())); + settings().setValue("touchActivateScreen", touchActivateScreen()); if (!getClientAddress().isEmpty()) { settings().setValue("clientAddress", getClientAddress()); @@ -182,6 +184,9 @@ void ServerConfig::recall() settings().value("clipboardSharingSize", (int)ServerConfig::defaultClipboardSharingSize()).toULongLong() ); setClipboardSharing(settings().value("clipboardSharing", true).toBool()); + setTouchActivateScreen( + settings().value("touchActivateScreen", + settings().value("touchInputLocal", false)).toBool()); setClientAddress(settings().value("clientAddress", "").toString()); readSettings(settings(), switchCorners(), "switchCorner", 0, static_cast(NumSwitchCorners)); @@ -310,6 +315,9 @@ QTextStream &operator<<(QTextStream &outStream, const ServerConfig &config) outStream << "\t" << "switchCornerSize = " << config.switchCornerSize() << Qt::endl; + outStream << "\t" + << "touchActivateScreen = " << (config.touchActivateScreen() ? "true" : "false") << Qt::endl; + foreach (const Hotkey &hotkey, config.hotkeys()) outStream << hotkey; diff --git a/src/gui/src/ServerConfig.h b/src/gui/src/ServerConfig.h index 2c99fa7fd..fc489e814 100644 --- a/src/gui/src/ServerConfig.h +++ b/src/gui/src/ServerConfig.h @@ -128,6 +128,10 @@ class ServerConfig : public ScreenConfig, public deskflow::gui::IServerConfig { return m_ClipboardSharingSize; } + bool touchActivateScreen() const + { + return m_TouchActivateScreen; + } static size_t defaultClipboardSharingSize(); // @@ -224,6 +228,10 @@ class ServerConfig : public ScreenConfig, public deskflow::gui::IServerConfig { m_ClipboardSharing = on; } + void setTouchActivateScreen(bool on) + { + m_TouchActivateScreen = on; + } void setConfigFile(const QString &configFile); void setUseExternalConfig(bool useExternalConfig); size_t setClipboardSharingSize(size_t size); @@ -253,6 +261,7 @@ class ServerConfig : public ScreenConfig, public deskflow::gui::IServerConfig int m_SwitchCornerSize = 0; bool m_DisableLockToScreen = false; bool m_ClipboardSharing = true; + bool m_TouchActivateScreen = false; QString m_ClientAddress = ""; QList m_SwitchCorners; HotkeyList m_Hotkeys; diff --git a/src/gui/src/ServerConfigDialog.cpp b/src/gui/src/ServerConfigDialog.cpp index e557d1788..9d5ad9da2 100644 --- a/src/gui/src/ServerConfigDialog.cpp +++ b/src/gui/src/ServerConfigDialog.cpp @@ -68,6 +68,7 @@ ServerConfigDialog::ServerConfigDialog(QWidget *parent, ServerConfig &config, Ap m_pCheckBoxCornerBottomRight->setChecked(serverConfig().switchCorner(static_cast(BottomRight))); m_pSpinBoxSwitchCornerSize->setValue(serverConfig().switchCornerSize()); m_pCheckBoxDisableLockToScreen->setChecked(serverConfig().disableLockToScreen()); + m_pCheckBoxTouchActivateScreen->setChecked(serverConfig().touchActivateScreen()); m_pCheckBoxEnableClipboard->setChecked(serverConfig().clipboardSharing()); int clipboardSharingSizeM = static_cast(serverConfig().clipboardSharingSize() / 1024); @@ -142,6 +143,10 @@ ServerConfigDialog::ServerConfigDialog(QWidget *parent, ServerConfig &config, Ap serverConfig().setDisableLockToScreen(v); onChange(); }); + connect(m_pCheckBoxTouchActivateScreen, &QCheckBox::stateChanged, this, [this](const int &v) { + serverConfig().setTouchActivateScreen(v); + onChange(); + }); connect(m_pCheckBoxCornerTopLeft, &QCheckBox::stateChanged, this, [this](const int &v) { serverConfig().setSwitchCorner(static_cast(TopLeft), v); onChange(); @@ -192,6 +197,10 @@ ServerConfigDialog::ServerConfigDialog(QWidget *parent, ServerConfig &config, Ap serverConfig().setDisableLockToScreen(v == Qt::Checked); onChange(); }); + connect(m_pCheckBoxTouchActivateScreen, &QCheckBox::checkStateChanged, this, [this](const Qt::CheckState &v) { + serverConfig().setTouchActivateScreen(v == Qt::Checked); + onChange(); + }); connect(m_pCheckBoxCornerTopLeft, &QCheckBox::checkStateChanged, this, [this](const Qt::CheckState &v) { serverConfig().setSwitchCorner(static_cast(TopLeft), v == Qt::Checked); onChange(); diff --git a/src/gui/src/ServerConfigDialogBase.ui b/src/gui/src/ServerConfigDialogBase.ui index a8a420821..98034b788 100644 --- a/src/gui/src/ServerConfigDialogBase.ui +++ b/src/gui/src/ServerConfigDialogBase.ui @@ -798,6 +798,16 @@ + + + + Switch screens on touch + + + Touch any screen to switch to that computer + + + @@ -1168,6 +1178,7 @@ Enabling this setting will disable the server config GUI. m_pCheckBoxWin32KeepForeground m_pCheckBoxIgnoreAutoConfigClient m_pCheckBoxDisableLockToScreen + m_pCheckBoxTouchActivateScreen m_pCheckBoxCornerTopLeft m_pCheckBoxCornerBottomLeft m_pCheckBoxCornerTopRight diff --git a/src/lib/base/EventTypes.cpp b/src/lib/base/EventTypes.cpp index 1c0d990b1..85896a3c7 100644 --- a/src/lib/base/EventTypes.cpp +++ b/src/lib/base/EventTypes.cpp @@ -115,6 +115,7 @@ REGISTER_EVENT(ClientListener, connected) REGISTER_EVENT(ClientProxy, ready) REGISTER_EVENT(ClientProxy, disconnected) +REGISTER_EVENT(ClientProxy, grabScreen) // // ClientProxyUnknown @@ -167,6 +168,7 @@ REGISTER_EVENT(IPrimaryScreen, hotKeyDown) REGISTER_EVENT(IPrimaryScreen, hotKeyUp) REGISTER_EVENT(IPrimaryScreen, fakeInputBegin) REGISTER_EVENT(IPrimaryScreen, fakeInputEnd) +REGISTER_EVENT(IPrimaryScreen, touchActivatedPrimary) // // IScreen @@ -176,6 +178,7 @@ REGISTER_EVENT(IScreen, error) REGISTER_EVENT(IScreen, shapeChanged) REGISTER_EVENT(IScreen, suspend) REGISTER_EVENT(IScreen, resume) +REGISTER_EVENT(IScreen, grabScreen) // // IpcServer diff --git a/src/lib/base/EventTypes.h b/src/lib/base/EventTypes.h index 6ddfe8adf..8885f1692 100644 --- a/src/lib/base/EventTypes.h +++ b/src/lib/base/EventTypes.h @@ -388,7 +388,7 @@ class ClientListenerEvents : public EventTypes class ClientProxyEvents : public EventTypes { public: - ClientProxyEvents() : m_ready(Event::kUnknown), m_disconnected(Event::kUnknown) + ClientProxyEvents() : m_ready(Event::kUnknown), m_disconnected(Event::kUnknown), m_grabScreen(Event::kUnknown) { } @@ -410,11 +410,20 @@ class ClientProxyEvents : public EventTypes */ Event::Type disconnected(); + //! Get grab screen event type + /*! + Returns the grab screen event type. This is sent when a client + requests to become the active screen (e.g., due to touch input). + Event data is MotionInfo* with the position where activation occurred. + */ + Event::Type grabScreen(); + //@} private: Event::Type m_ready; Event::Type m_disconnected; + Event::Type m_grabScreen; }; class ClientProxyUnknownEvents : public EventTypes @@ -603,7 +612,8 @@ class IPrimaryScreenEvents : public EventTypes m_hotKeyDown(Event::kUnknown), m_hotKeyUp(Event::kUnknown), m_fakeInputBegin(Event::kUnknown), - m_fakeInputEnd(Event::kUnknown) + m_fakeInputEnd(Event::kUnknown), + m_touchActivatedPrimary(Event::kUnknown) { } @@ -650,6 +660,13 @@ class IPrimaryScreenEvents : public EventTypes //! end of fake input event type Event::Type fakeInputEnd(); + //! touch activated primary screen event type + /*! + Event data is MotionInfo* with the position where touch occurred. + This is sent when touch input on the primary screen should activate it. + */ + Event::Type touchActivatedPrimary(); + //@} private: @@ -664,6 +681,7 @@ class IPrimaryScreenEvents : public EventTypes Event::Type m_hotKeyUp; Event::Type m_fakeInputBegin; Event::Type m_fakeInputEnd; + Event::Type m_touchActivatedPrimary; }; class IScreenEvents : public EventTypes @@ -673,7 +691,8 @@ class IScreenEvents : public EventTypes : m_error(Event::kUnknown), m_shapeChanged(Event::kUnknown), m_suspend(Event::kUnknown), - m_resume(Event::kUnknown) + m_resume(Event::kUnknown), + m_grabScreen(Event::kUnknown) { } @@ -708,6 +727,14 @@ class IScreenEvents : public EventTypes */ Event::Type resume(); + //! Get grab screen event type + /*! + Returns the grab screen event type. This is sent when a secondary screen + requests to become the active screen (e.g., due to touch input). + Event data is MotionInfo* with the position where activation occurred. + */ + Event::Type grabScreen(); + //@} private: @@ -715,6 +742,7 @@ class IScreenEvents : public EventTypes Event::Type m_shapeChanged; Event::Type m_suspend; Event::Type m_resume; + Event::Type m_grabScreen; }; class ClipboardEvents : public EventTypes diff --git a/src/lib/client/Client.cpp b/src/lib/client/Client.cpp index d23077f6b..b45c107ae 100644 --- a/src/lib/client/Client.cpp +++ b/src/lib/client/Client.cpp @@ -29,6 +29,7 @@ #include "deskflow/DropHelper.h" #include "deskflow/FileChunk.h" #include "deskflow/IPlatformScreen.h" +#include "deskflow/IPrimaryScreen.h" #include "deskflow/PacketStreamFilter.h" #include "deskflow/ProtocolUtil.h" #include "deskflow/Screen.h" @@ -87,6 +88,10 @@ Client::Client( m_events->adoptHandler( m_events->forIScreen().resume(), getEventTarget(), new TMethodEventJob(this, &Client::handleResume) ); + m_events->adoptHandler( + m_events->forIScreen().grabScreen(), m_screen->getEventTarget(), + new TMethodEventJob(this, &Client::handleGrabScreen) + ); if (m_args.m_enableDragDrop) { m_events->adoptHandler( @@ -107,6 +112,7 @@ Client::~Client() m_events->removeHandler(m_events->forIScreen().suspend(), getEventTarget()); m_events->removeHandler(m_events->forIScreen().resume(), getEventTarget()); + m_events->removeHandler(m_events->forIScreen().grabScreen(), m_screen->getEventTarget()); cleanupTimer(); cleanupScreen(); @@ -719,6 +725,16 @@ void Client::handleResume(const Event &, void *) } } +void Client::handleGrabScreen(const Event &event, void *) +{ + // Forward grab screen request to server via protocol + IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); + if (m_server != NULL) { + LOG((CLOG_DEBUG1 "requesting screen grab at %d,%d", info->m_x, info->m_y)); + m_server->grabScreen(info->m_x, info->m_y); + } +} + void Client::handleFileChunkSending(const Event &event, void *) { sendFileChunk(event.getDataObject()); diff --git a/src/lib/client/Client.h b/src/lib/client/Client.h index e0f341ad5..a80b2f7b9 100644 --- a/src/lib/client/Client.h +++ b/src/lib/client/Client.h @@ -220,6 +220,7 @@ class Client : public IClient, public INode void handleHello(const Event &, void *); void handleSuspend(const Event &event, void *); void handleResume(const Event &event, void *); + void handleGrabScreen(const Event &event, void *); void handleFileChunkSending(const Event &, void *); void handleFileRecieveCompleted(const Event &, void *); void handleStopRetry(const Event &, void *); diff --git a/src/lib/client/ServerProxy.cpp b/src/lib/client/ServerProxy.cpp index e0b4cb853..e57a6e1c4 100644 --- a/src/lib/client/ServerProxy.cpp +++ b/src/lib/client/ServerProxy.cpp @@ -372,6 +372,12 @@ bool ServerProxy::onGrabClipboard(ClipboardID id) return true; } +void ServerProxy::grabScreen(SInt32 x, SInt32 y) +{ + LOG((CLOG_DEBUG1 "requesting screen grab at %d,%d", x, y)); + ProtocolUtil::writef(m_stream, kMsgCGrabScreen, x, y); +} + void ServerProxy::onClipboardChanged(ClipboardID id, const IClipboard *clipboard) { String data = IClipboard::marshall(clipboard); diff --git a/src/lib/client/ServerProxy.h b/src/lib/client/ServerProxy.h index 743c3393f..c02816e3e 100644 --- a/src/lib/client/ServerProxy.h +++ b/src/lib/client/ServerProxy.h @@ -61,6 +61,13 @@ class ServerProxy bool onGrabClipboard(ClipboardID); void onClipboardChanged(ClipboardID, const IClipboard *); + //! Request to grab screen + /*! + Sends a request to the server to make this client the active screen. + This is typically called when touch input is detected on the client. + */ + void grabScreen(SInt32 x, SInt32 y); + //@} // sending file chunk to server diff --git a/src/lib/deskflow/option_types.h b/src/lib/deskflow/option_types.h index 99db87a47..20135995c 100644 --- a/src/lib/deskflow/option_types.h +++ b/src/lib/deskflow/option_types.h @@ -69,6 +69,7 @@ static const OptionID kOptionWin32KeepForeground = OPTION_CODE("_KFW"); static const OptionID kOptionDisableLockToScreen = OPTION_CODE("DLTS"); static const OptionID kOptionClipboardSharing = OPTION_CODE("CLPS"); static const OptionID kOptionClipboardSharingSize = OPTION_CODE("CLSZ"); +static const OptionID kOptionTouchActivateScreen = OPTION_CODE("TILC"); //@} //! @name Screen switch corner enumeration diff --git a/src/lib/deskflow/protocol_types.cpp b/src/lib/deskflow/protocol_types.cpp index 72cbf0973..fbe6a4116 100644 --- a/src/lib/deskflow/protocol_types.cpp +++ b/src/lib/deskflow/protocol_types.cpp @@ -50,6 +50,7 @@ const char *const kMsgDFileTransfer = "DFTR%1i%s"; const char *const kMsgDDragInfo = "DDRG%2i%s"; const char *const kMsgDSecureInputNotification = "SECN%s"; const char *const kMsgDLanguageSynchronisation = "LSYN%s"; +const char *const kMsgCGrabScreen = "CGRB%2i%2i"; const char *const kMsgQInfo = "QINF"; const char *const kMsgEIncompatible = "EICV%2i%2i"; const char *const kMsgEBusy = "EBSY"; diff --git a/src/lib/deskflow/protocol_types.h b/src/lib/deskflow/protocol_types.h index 5c67bb161..6b8e609fd 100644 --- a/src/lib/deskflow/protocol_types.h +++ b/src/lib/deskflow/protocol_types.h @@ -294,6 +294,11 @@ extern const char *const kMsgDSecureInputNotification; // $1 = List of server languages extern const char *const kMsgDLanguageSynchronisation; +// grab screen request: secondary -> primary +// Client requests to become the active screen (e.g., due to touch input). +// $1 = x position, $2 = y position where activation occurred +extern const char *const kMsgCGrabScreen; + // // query codes // diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index c9afc4d31..85e4e35e6 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -414,20 +414,12 @@ LRESULT CALLBACK MSWindowsDesks::primaryDeskProc(HWND hwnd, UINT msg, WPARAM wPa LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { - // would like to detect any local user input and hide the hider - // window but for now we just detect mouse motion. - bool hide = false; switch (msg) { - case WM_MOUSEMOVE: - if (LOWORD(lParam) != 0 || HIWORD(lParam) != 0) { - hide = true; - } - break; - } - - if (hide && IsWindowVisible(hwnd)) { - ReleaseCapture(); - SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + case WM_SETCURSOR: + // Force blank cursor. On touchscreen devices, ShowCursor(FALSE) may + // not reliably hide the cursor, so we also set a NULL cursor here. + SetCursor(NULL); + return TRUE; } return DefWindowProc(hwnd, msg, wParam, lParam); @@ -519,6 +511,10 @@ void MSWindowsDesks::deskEnter(Desk *desk) { if (!m_isPrimary) { ReleaseCapture(); + + // Restore WS_EX_TRANSPARENT that was removed in deskLeave + LONG_PTR exStyle = GetWindowLongPtr(desk->m_window, GWL_EXSTYLE); + SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle | WS_EX_TRANSPARENT); } setCursorVisibility(true); @@ -600,23 +596,21 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) } } } else { - // move hider window under the cursor center, raise, and show it - SetWindowPos(desk->m_window, HWND_TOP, m_xCenter, m_yCenter, 1, 1, SWP_NOACTIVATE | SWP_SHOWWINDOW); + // Remove WS_EX_TRANSPARENT so the hider window receives hit-testing + // and its blank cursor class applies. Without this, hit-testing + // passes through and the cursor of the window behind is shown. + LONG_PTR exStyle = GetWindowLongPtr(desk->m_window, GWL_EXSTYLE); + SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle & ~WS_EX_TRANSPARENT); + + // Cover the entire screen with the hider window so the blank cursor + // class applies everywhere. On touchscreen devices, ShowCursor(FALSE) + // is unreliable, so the blank cursor on a full-screen window is the + // primary hiding mechanism. + SetWindowPos(desk->m_window, HWND_TOPMOST, m_x, m_y, m_w, m_h, SWP_NOACTIVATE | SWP_SHOWWINDOW); - // watch for mouse motion. if we see any then we hide the - // hider window so the user can use the physically attached - // mouse if desired. we'd rather not capture the mouse but - // we aren't notified when the mouse leaves our window. SetCapture(desk->m_window); - // windows can take a while to hide the cursor, so wait a few milliseconds to ensure the cursor - // is hidden before centering. this doesn't seem to affect the fluidity of the transition. - // without this, the cursor appears to flicker in the center of the screen which is annoying. - // a slightly more elegant but complex solution could be to use a timed event. - // 30 ms seems to work well enough without making the transition feel janky; a lower number - // would be better but 10 ms doesn't seem to be quite long enough, as we get noticeable flicker. - // this is largely a balance and out of our control, since windows can be unpredictable... - // maybe another approach would be to repeatedly check the cursor visibility until it is hidden. + // Brief delay for cursor hiding to take effect, then center the cursor. LOG_DEBUG1("centering cursor on leave: %+d,%+d", m_xCenter, m_yCenter); ARCH->sleep(0.03); deskMouseMove(m_xCenter, m_yCenter); @@ -640,6 +634,33 @@ void MSWindowsDesks::deskThread(void *vdesk) try { desk->m_window = createWindow(m_deskClass, DESKFLOW_APP_NAME "Desk"); LOG((CLOG_DEBUG "desk %s window is 0x%08x", desk->m_name.c_str(), desk->m_window)); + + // Register for raw touch input on the desk window. This MUST be on + // the desk thread (not the main thread) because the main event loop + // uses QS_ALLPOSTMESSAGE which never wakes for WM_INPUT messages. + // The desk thread's GetMessage(NULL,0,0) has no such filter. + RAWINPUTDEVICE rids[4] = {}; + rids[0].usUsagePage = 0x0D; rids[0].usUsage = 0x04; // Touch Screen + rids[0].dwFlags = RIDEV_INPUTSINK; rids[0].hwndTarget = desk->m_window; + rids[1].usUsagePage = 0x0D; rids[1].usUsage = 0x05; // Touch Pad + rids[1].dwFlags = RIDEV_INPUTSINK; rids[1].hwndTarget = desk->m_window; + rids[2].usUsagePage = 0x0D; rids[2].usUsage = 0x01; // Digitizer + rids[2].dwFlags = RIDEV_INPUTSINK; rids[2].hwndTarget = desk->m_window; + rids[3].usUsagePage = 0x0D; rids[3].usUsage = 0x02; // Pen + rids[3].dwFlags = RIDEV_INPUTSINK; rids[3].hwndTarget = desk->m_window; + if (RegisterRawInputDevices(rids, 4, sizeof(RAWINPUTDEVICE))) { + LOG((CLOG_DEBUG "desk %s: registered raw touch input on desk window", + desk->m_name.c_str())); + } else { + // fallback: try just touch screen + if (RegisterRawInputDevices(rids, 1, sizeof(RAWINPUTDEVICE))) { + LOG((CLOG_DEBUG "desk %s: registered touch screen raw input", + desk->m_name.c_str())); + } else { + LOG((CLOG_WARN "desk %s: failed to register raw touch input, error=%d", + desk->m_name.c_str(), GetLastError())); + } + } } catch (...) { // ignore LOG((CLOG_DEBUG "can't create desk window for %s", desk->m_name.c_str())); @@ -660,6 +681,32 @@ void MSWindowsDesks::deskThread(void *vdesk) DispatchMessage(&msg); continue; + case WM_INPUT: { + // Raw touch input from digitizer. Forward to main thread as + // DESKFLOW_MSG_TOUCH for the same handling as the LL mouse hook. + UINT size = 0; + GetRawInputData( + reinterpret_cast(msg.lParam), RID_INPUT, + NULL, &size, sizeof(RAWINPUTHEADER)); + if (size > 0 && size <= 1024) { + BYTE buffer[1024]; + if (GetRawInputData( + reinterpret_cast(msg.lParam), RID_INPUT, + buffer, &size, sizeof(RAWINPUTHEADER)) != static_cast(-1)) { + RAWINPUT *raw = reinterpret_cast(buffer); + if (raw->header.dwType == RIM_TYPEHID && + raw->data.hid.dwCount > 0 && raw->data.hid.dwSizeHid > 0) { + POINT pt; + GetCursorPos(&pt); + LOG((CLOG_DEBUG1 "desk raw touch at %d,%d", pt.x, pt.y)); + PostThreadMessage(m_threadID, DESKFLOW_MSG_TOUCH, + static_cast(pt.x), static_cast(pt.y)); + } + } + } + continue; + } + case DESKFLOW_MSG_SWITCH: if (!m_noHooks) { MSWindowsHook::uninstall(); diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index b51b1a734..e042fab41 100644 --- a/src/lib/platform/MSWindowsHook.cpp +++ b/src/lib/platform/MSWindowsHook.cpp @@ -44,6 +44,13 @@ static BYTE g_keyState[256] = {0}; static DWORD g_hookThread = 0; static bool g_fakeServerInput = false; static BOOL g_isPrimary = TRUE; +static bool g_touchActivateScreen = false; + +// Microsoft touch signature in dwExtraInfo (MI_WP_SIGNATURE). +// The upper 24 bits (masked by 0xFFFFFF00) identify touch-generated +// mouse events; the lower 8 bits contain pen/touch flags. +#define TOUCH_SIGNATURE_MASK 0xFFFFFF00 +#define TOUCH_SIGNATURE 0xFF515700 MSWindowsHook::MSWindowsHook() { @@ -148,6 +155,16 @@ void MSWindowsHook::setMode(EHookMode mode) g_mode = mode; } +void MSWindowsHook::setTouchActivateScreen(bool enabled) +{ + g_touchActivateScreen = enabled; +} + +void MSWindowsHook::setIsPrimary(bool primary) +{ + g_isPrimary = primary ? TRUE : FALSE; +} + static void keyboardGetState(BYTE keys[256], DWORD vkCode, bool kf_up) { // we have to use GetAsyncKeyState() rather than GetKeyState() because @@ -580,6 +597,25 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) // decode the message MSLLHOOKSTRUCT *info = reinterpret_cast(lParam); + // detect touch-originated mouse events via dwExtraInfo signature. + // this must run before the injected check, because Windows marks + // touch-synthesized mouse events as injected (LLMHF_INJECTED). + if (g_touchActivateScreen) { + bool isTouchEvent = (info->dwExtraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE; + if (isTouchEvent && (wParam == WM_LBUTTONDOWN || wParam == WM_MOUSEMOVE)) { + SInt32 x = static_cast(info->pt.x); + SInt32 y = static_cast(info->pt.y); + PostThreadMessage(g_threadID, DESKFLOW_MSG_TOUCH, x, y); + // On primary: eat the event to prevent edge detection and + // button-state locking (isLockedToScreen) from racing. + // On secondary (client): let it through so the click reaches + // the target window (e.g. Start menu) — no jump zones on clients. + if (g_isPrimary) { + return 1; + } + } + } + bool const injected = info->flags & LLMHF_INJECTED; if (!g_isPrimary && injected) { return CallNextHookEx(g_mouseLL, code, wParam, lParam); diff --git a/src/lib/platform/MSWindowsHook.h b/src/lib/platform/MSWindowsHook.h index 51684395f..8aac00950 100644 --- a/src/lib/platform/MSWindowsHook.h +++ b/src/lib/platform/MSWindowsHook.h @@ -49,4 +49,8 @@ class MSWindowsHook static int installScreenSaver(); static int uninstallScreenSaver(); + + void setTouchActivateScreen(bool enabled); + + void setIsPrimary(bool primary); }; diff --git a/src/lib/platform/MSWindowsScreen.cpp b/src/lib/platform/MSWindowsScreen.cpp index f585110cf..46be497bf 100644 --- a/src/lib/platform/MSWindowsScreen.cpp +++ b/src/lib/platform/MSWindowsScreen.cpp @@ -32,6 +32,7 @@ #include "deskflow/Clipboard.h" #include "deskflow/KeyMap.h" #include "deskflow/XScreen.h" +#include "deskflow/option_types.h" #include "mt/Thread.h" #include "platform/MSWindowsClipboard.h" #include "platform/MSWindowsDesks.h" @@ -83,6 +84,28 @@ #define PBT_APMRESUMEAUTOMATIC 0x0012 #endif +// WM_POINTER stuff (Windows 8+) +#if !defined(WM_POINTERDOWN) +#define WM_POINTERDOWN 0x0246 +#define WM_POINTERUP 0x0247 +#define WM_POINTERUPDATE 0x0245 +#define WM_POINTERENTER 0x0249 +#define WM_POINTERLEAVE 0x024A +#define GET_POINTERID_WPARAM(wParam) (LOWORD(wParam)) +#endif + +#if !defined(PT_POINTER) +#define PT_POINTER 1 +#define PT_TOUCH 2 +#define PT_PEN 3 +#define PT_MOUSE 4 +#endif + +// Function pointer type for GetPointerType (loaded dynamically for Win7 compat) +typedef BOOL(WINAPI *GetPointerTypeFunc)(UINT32 pointerId, DWORD *pointerType); +static GetPointerTypeFunc s_getPointerType = NULL; +static bool s_pointerApiChecked = false; + // // MSWindowsScreen // @@ -124,7 +147,9 @@ MSWindowsScreen::MSWindowsScreen( m_hasMouse(GetSystemMetrics(SM_MOUSEPRESENT) != 0), m_events(events), m_dropWindow(NULL), - m_dropWindowSize(20) + m_dropWindowSize(20), + m_touchActivateScreen(false), + m_touchDebounceTimer() { LOG_DEBUG("settting up %s screen", m_isPrimary ? "primary" : "secondary"); @@ -133,8 +158,9 @@ MSWindowsScreen::MSWindowsScreen( s_screen = this; try { - if (m_isPrimary && !m_noHooks) { + if (!m_noHooks) { m_hook.loadLibrary(); + m_hook.setIsPrimary(m_isPrimary); } m_screensaver = new MSWindowsScreenSaver(); @@ -150,6 +176,7 @@ MSWindowsScreen::MSWindowsScreen( m_class = createWindowClass(); m_window = createWindow(m_class, DESKFLOW_APP_NAME); setupMouseKeys(); + LOG((CLOG_DEBUG "screen shape: %d,%d %dx%d %s", m_x, m_y, m_w, m_h, m_multimon ? "(multi-monitor)" : "")); LOG((CLOG_DEBUG "window is 0x%08x", m_window)); @@ -476,6 +503,15 @@ void MSWindowsScreen::resetOptions() void MSWindowsScreen::setOptions(const OptionsList &options) { m_desks->setOptions(options); + + // check for touch input local option + for (UInt32 i = 0, n = (UInt32)options.size(); i < n; i += 2) { + if (options[i] == kOptionTouchActivateScreen) { + m_touchActivateScreen = (options[i + 1] != 0); + m_hook.setTouchActivateScreen(m_touchActivateScreen); + LOG((CLOG_DEBUG "touch activate screen set to %s", m_touchActivateScreen ? "true" : "false")); + } + } } void MSWindowsScreen::setSequenceNumber(UInt32 seqNum) @@ -957,6 +993,30 @@ bool MSWindowsScreen::onPreDispatch(HWND hwnd, UINT message, WPARAM wParam, LPAR case DESKFLOW_MSG_DEBUG: LOG((CLOG_DEBUG1 "hook: 0x%08x 0x%08x", wParam, lParam)); return true; + + case DESKFLOW_MSG_TOUCH: + // Thread messages (PostThreadMessage) bypass DispatchMessage, so + // this must be handled here in onPreDispatch, not in onEvent. + if (!m_touchActivateScreen || m_isOnScreen) + return true; + { + if (m_touchDebounceTimer.getTime() < kTouchDebounceTime) + return true; + m_touchDebounceTimer.reset(); + + SInt32 x = static_cast(wParam); + SInt32 y = static_cast(lParam); + if (m_isPrimary) { + LOG((CLOG_INFO "hook: touch activating primary screen at %d,%d", x, y)); + sendEvent(m_events->forIPrimaryScreen().touchActivatedPrimary(), + MotionInfo::alloc(x, y)); + } else { + LOG((CLOG_INFO "hook: touch requesting screen grab at %d,%d", x, y)); + sendEvent(m_events->forIScreen().grabScreen(), + MotionInfo::alloc(x, y)); + } + } + return true; } if (m_isPrimary) { @@ -1043,6 +1103,17 @@ bool MSWindowsScreen::onEvent(HWND, UINT msg, WPARAM wParam, LPARAM lParam, LRES case WM_DISPLAYCHANGE: return onDisplayChange(); + case WM_POINTERDOWN: + case WM_POINTERUP: + case WM_POINTERUPDATE: + if (onPointerInput(wParam, lParam)) { + // Touch input was consumed (kept local or triggered screen switch) + *result = 0; + return true; + } + // Fall through to let DefWindowProc convert to mouse messages + return false; + /* On windows 10 we don't receive WM_POWERBROADCAST after sleep. We receive only WM_TIMECHANGE hence this message is used to resume.*/ case WM_TIMECHANGE: @@ -1408,6 +1479,60 @@ bool MSWindowsScreen::onScreensaver(bool activated) return true; } +bool MSWindowsScreen::isPointerTypeTouch(UINT32 pointerId) const +{ + // Dynamically load GetPointerType for Windows 7 compatibility + if (!s_pointerApiChecked) { + s_pointerApiChecked = true; + HMODULE user32 = GetModuleHandle("user32.dll"); + if (user32 != NULL) { + s_getPointerType = (GetPointerTypeFunc)GetProcAddress(user32, "GetPointerType"); + } + } + + if (s_getPointerType == NULL) { + // API not available (Windows 7 or earlier) + return false; + } + + DWORD pointerType = PT_POINTER; + if (s_getPointerType(pointerId, &pointerType)) { + return (pointerType == PT_TOUCH || pointerType == PT_PEN); + } + return false; +} + +bool MSWindowsScreen::onPointerInput(WPARAM wParam, LPARAM lParam) +{ + UINT32 pointerId = GET_POINTERID_WPARAM(wParam); + + if (!isPointerTypeTouch(pointerId)) + return false; + + if (!m_touchActivateScreen || m_isOnScreen) + return false; + + if (m_touchDebounceTimer.getTime() < kTouchDebounceTime) + return true; + m_touchDebounceTimer.reset(); + + POINT pt; + if (!GetCursorPos(&pt)) + return false; + + if (m_isPrimary) { + LOG((CLOG_INFO "touch activating primary screen at %d,%d", pt.x, pt.y)); + sendEvent(m_events->forIPrimaryScreen().touchActivatedPrimary(), + MotionInfo::alloc(pt.x, pt.y)); + } else { + LOG((CLOG_INFO "touch requesting screen grab at %d,%d", pt.x, pt.y)); + sendEvent(m_events->forIScreen().grabScreen(), + MotionInfo::alloc(pt.x, pt.y)); + } + + return true; +} + bool MSWindowsScreen::onDisplayChange() { // screen resolution may have changed. save old shape. diff --git a/src/lib/platform/MSWindowsScreen.h b/src/lib/platform/MSWindowsScreen.h index 8688d4286..9c98089ee 100644 --- a/src/lib/platform/MSWindowsScreen.h +++ b/src/lib/platform/MSWindowsScreen.h @@ -18,6 +18,7 @@ #pragma once +#include "base/Stopwatch.h" #include "base/String.h" #include "deskflow/ClientArgs.h" #include "deskflow/DragInformation.h" @@ -190,6 +191,8 @@ class MSWindowsScreen : public PlatformScreen bool onScreensaver(bool activated); bool onDisplayChange(); bool onClipboardChange(); + bool onPointerInput(WPARAM wParam, LPARAM lParam); + bool isPointerTypeTouch(UINT32 pointerId) const; // warp cursor without discarding queued events void warpCursorNoFlush(SInt32 x, SInt32 y); @@ -357,4 +360,11 @@ class MSWindowsScreen : public PlatformScreen PrimaryKeyDownList m_primaryKeyDownList; MSWindowsPowerManager m_powerManager; + + // When true, touching this screen activates it (switches focus here) + bool m_touchActivateScreen; + + // Debounce rapid touch events to prevent multiple switch requests + Stopwatch m_touchDebounceTimer; + static constexpr double kTouchDebounceTime = 0.15; // 150ms debounce }; diff --git a/src/lib/platform/dfwhook.h b/src/lib/platform/dfwhook.h index 8822663ae..7280ac5fb 100644 --- a/src/lib/platform/dfwhook.h +++ b/src/lib/platform/dfwhook.h @@ -46,9 +46,10 @@ #define DESKFLOW_MSG_PRE_WARP WM_APP + 0x0017 // x; y #define DESKFLOW_MSG_SCREEN_SAVER WM_APP + 0x0018 // activated; #define DESKFLOW_MSG_DEBUG WM_APP + 0x0019 // data, data +#define DESKFLOW_MSG_TOUCH WM_APP + 0x001A // x; y (touch-originated mouse event) #define DESKFLOW_MSG_INPUT_FIRST DESKFLOW_MSG_KEY #define DESKFLOW_MSG_INPUT_LAST DESKFLOW_MSG_PRE_WARP -#define DESKFLOW_HOOK_LAST_MSG DESKFLOW_MSG_DEBUG +#define DESKFLOW_HOOK_LAST_MSG DESKFLOW_MSG_TOUCH #define DESKFLOW_HOOK_FAKE_INPUT_VIRTUAL_KEY VK_CANCEL #define DESKFLOW_HOOK_FAKE_INPUT_SCANCODE 0 diff --git a/src/lib/server/ClientProxy1_0.cpp b/src/lib/server/ClientProxy1_0.cpp index 31b51020f..7d1deef57 100644 --- a/src/lib/server/ClientProxy1_0.cpp +++ b/src/lib/server/ClientProxy1_0.cpp @@ -21,6 +21,7 @@ #include "base/IEventQueue.h" #include "base/Log.h" #include "base/TMethodEventJob.h" +#include "deskflow/IPrimaryScreen.h" #include "deskflow/ProtocolUtil.h" #include "deskflow/XDeskflow.h" #include "io/IStream.h" @@ -188,10 +189,31 @@ bool ClientProxy1_0::parseMessage(const UInt8 *code) return recvGrabClipboard(); } else if (memcmp(code, kMsgDClipboard, 4) == 0) { return recvClipboard(); + } else if (memcmp(code, kMsgCGrabScreen, 4) == 0) { + return recvGrabScreen(); } return false; } +bool ClientProxy1_0::recvGrabScreen() +{ + // parse message + SInt16 x, y; + if (!ProtocolUtil::readf(getStream(), kMsgCGrabScreen + 4, &x, &y)) { + return false; + } + LOG((CLOG_DEBUG "received client \"%s\" grab screen request at %d,%d", getName().c_str(), x, y)); + + // notify server to switch to this client + m_events->addEvent(Event( + m_events->forClientProxy().grabScreen(), + getEventTarget(), + IPrimaryScreen::MotionInfo::alloc(x, y) + )); + + return true; +} + void ClientProxy1_0::handleDisconnect(const Event &, void *) { LOG((CLOG_NOTE "client \"%s\" has disconnected", getName().c_str())); diff --git a/src/lib/server/ClientProxy1_0.h b/src/lib/server/ClientProxy1_0.h index 0339b44e6..bfa6e4e71 100644 --- a/src/lib/server/ClientProxy1_0.h +++ b/src/lib/server/ClientProxy1_0.h @@ -87,6 +87,7 @@ class ClientProxy1_0 : public ClientProxy bool recvInfo(); bool recvGrabClipboard(); + bool recvGrabScreen(); protected: struct ClientClipboard diff --git a/src/lib/server/Config.cpp b/src/lib/server/Config.cpp index aeeef31b4..b72eaa226 100644 --- a/src/lib/server/Config.cpp +++ b/src/lib/server/Config.cpp @@ -699,6 +699,8 @@ void Config::readSectionOptions(ConfigReadContext &s) addOption("", kOptionClipboardSharing, s.parseBoolean(value)); } else if (name == "clipboardSharingSize") { addOption("", kOptionClipboardSharingSize, s.parseInt(value)); + } else if (name == "touchActivateScreen" || name == "touchInputLocal") { + addOption("", kOptionTouchActivateScreen, s.parseBoolean(value)); } else if (name == "clientAddress") { m_ClientAddress = value; } else { diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index 43a5fbae1..a8651360a 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -40,12 +40,18 @@ #include "server/ClientProxyUnknown.h" #include "server/PrimaryClient.h" +#include #include #include #include #include #include +#if WINAPI_MSWINDOWS +#define WIN32_LEAN_AND_MEAN +#include +#endif + using namespace deskflow::server; // @@ -176,6 +182,10 @@ Server::Server( m_events->forIPrimaryScreen().fakeInputEnd(), m_inputFilter, new TMethodEventJob(this, &Server::handleFakeInputEndEvent) ); + m_events->adoptHandler( + m_events->forIPrimaryScreen().touchActivatedPrimary(), m_primaryClient->getEventTarget(), + new TMethodEventJob(this, &Server::handleTouchActivatedPrimaryEvent) + ); if (m_args.m_enableDragDrop) { m_events->adoptHandler( @@ -225,6 +235,7 @@ Server::~Server() m_events->removeHandler(m_events->forIPrimaryScreen().screensaverDeactivated(), m_primaryClient->getEventTarget()); m_events->removeHandler(m_events->forIPrimaryScreen().fakeInputBegin(), m_inputFilter); m_events->removeHandler(m_events->forIPrimaryScreen().fakeInputEnd(), m_inputFilter); + m_events->removeHandler(m_events->forIPrimaryScreen().touchActivatedPrimary(), m_primaryClient->getEventTarget()); m_events->removeHandler(Event::kTimer, this); stopSwitch(); @@ -776,6 +787,15 @@ bool Server::isSwitchOkay( return false; } + // check if we're in touch switch cooldown period + // this prevents edge-triggered switches from immediately undoing + // a touch-triggered switch (which causes rapid bounce switching) + if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { + LOG((CLOG_DEBUG1 "edge switch blocked by touch cooldown (%.2fs remaining)", + kTouchSwitchCooldownTime - m_touchSwitchCooldown.getTime())); + return false; + } + // should we switch or not? bool preventSwitch = false; bool allowSwitch = false; @@ -1337,6 +1357,120 @@ void Server::handleSwitchInDirectionEvent(const Event &event, void *) } } +void Server::handleTouchActivatedPrimaryEvent(const Event &event, void *) +{ + IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); + LOG((CLOG_DEBUG1 "touch activated primary at %d,%d", info->m_x, info->m_y)); + + // reject if still in cooldown from a recent touch switch + if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { + LOG((CLOG_DEBUG1 "touch switch rejected (cooldown active)")); + return; + } + + if (m_active != m_primaryClient) { + // Save current cursor position on the screen we're leaving + // (same as jumpToScreen does for edge-triggered switches) + m_active->setJumpCursorPos(m_x, m_y); + + // Clamp touch coordinates away from screen edges to avoid landing + // in the jump zone, which would trigger an immediate edge switch + SInt32 x = info->m_x; + SInt32 y = info->m_y; + SInt32 dx, dy, dw, dh; + m_primaryClient->getShape(dx, dy, dw, dh); + SInt32 z = getJumpZoneSize(m_primaryClient) + 1; + x = (std::max)(x, dx + z); + x = (std::min)(x, dx + dw - 1 - z); + y = (std::max)(y, dy + z); + y = (std::min)(y, dy + dh - 1 - z); + + // Switch back to primary screen at clamped touch position + switchScreen(m_primaryClient, x, y, false); + + // Synthesize a click at the touch position to focus the target window. + // The hook eats the original touch event to prevent edge detection race, + // so without this the window under the touch point never receives focus. + m_primaryClient->mouseDown(kButtonLeft); + m_primaryClient->mouseUp(kButtonLeft); + + // Force the window under the touch point to the foreground. + // The hook eats the original touch event, so the target window + // never receives the click. We must explicitly activate it. + // Windows restricts SetForegroundWindow to prevent focus stealing, + // so we use AttachThreadInput to bypass the restriction. +#if WINAPI_MSWINDOWS + { + POINT pt = { x, y }; + HWND hwnd = WindowFromPoint(pt); + if (hwnd != NULL) { + HWND root = GetAncestor(hwnd, GA_ROOT); + if (root != NULL) { + DWORD foreThread = GetWindowThreadProcessId( + GetForegroundWindow(), NULL); + DWORD curThread = GetCurrentThreadId(); + if (foreThread != curThread) { + AttachThreadInput(foreThread, curThread, TRUE); + } + SetForegroundWindow(root); + if (foreThread != curThread) { + AttachThreadInput(foreThread, curThread, FALSE); + } + LOG((CLOG_DEBUG1 "touch: forced foreground window 0x%08x", root)); + } + } + } +#endif + + // Start cooldown to prevent edge-triggered switches from immediately + // undoing this touch-triggered switch + m_touchSwitchCooldown.reset(); + LOG((CLOG_DEBUG1 "touch switch cooldown started")); + } +} + +void Server::handleGrabScreenEvent(const Event &event, void *vclient) +{ + IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); + BaseClientProxy *client = static_cast(vclient); + + LOG((CLOG_DEBUG1 "client \"%s\" requests grab at %d,%d", getName(client).c_str(), info->m_x, info->m_y)); + + // reject if still in cooldown from a recent touch switch + if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { + LOG((CLOG_DEBUG1 "grab rejected (cooldown active)")); + return; + } + + if (client != m_active) { + // Save current cursor position on the screen we're leaving + // (same as jumpToScreen does for edge-triggered switches) + m_active->setJumpCursorPos(m_x, m_y); + + // Clamp touch coordinates away from screen edges to avoid landing + // in the jump zone on the primary screen (which would trigger an + // immediate edge switch back). Only matters when switching away + // from primary, since jump zones only exist on primary. + SInt32 x = info->m_x; + SInt32 y = info->m_y; + SInt32 dx, dy, dw, dh; + client->getShape(dx, dy, dw, dh); + SInt32 z = getJumpZoneSize(client) + 1; + x = (std::max)(x, dx + z); + x = (std::min)(x, dx + dw - 1 - z); + y = (std::max)(y, dy + z); + y = (std::min)(y, dy + dh - 1 - z); + + // Switch to the requesting client at clamped touch position + switchScreen(client, x, y, false); + + // Start cooldown to prevent edge-triggered switches from immediately + // undoing this touch-triggered switch + m_touchSwitchCooldown.reset(); + LOG((CLOG_DEBUG1 "touch switch cooldown started")); + } +} + void Server::handleKeyboardBroadcastEvent(const Event &event, void *) { KeyboardBroadcastInfo *info = (KeyboardBroadcastInfo *)event.getData(); @@ -1989,6 +2123,10 @@ bool Server::addClient(BaseClientProxy *client) m_events->forClipboard().clipboardChanged(), client->getEventTarget(), new TMethodEventJob(this, &Server::handleClipboardChanged, client) ); + m_events->adoptHandler( + m_events->forClientProxy().grabScreen(), client->getEventTarget(), + new TMethodEventJob(this, &Server::handleGrabScreenEvent, client) + ); // add to list m_clientSet.insert(client); @@ -2017,6 +2155,7 @@ bool Server::removeClient(BaseClientProxy *client) m_events->removeHandler(m_events->forIScreen().shapeChanged(), client->getEventTarget()); m_events->removeHandler(m_events->forClipboard().clipboardGrabbed(), client->getEventTarget()); m_events->removeHandler(m_events->forClipboard().clipboardChanged(), client->getEventTarget()); + m_events->removeHandler(m_events->forClientProxy().grabScreen(), client->getEventTarget()); // remove from list m_clients.erase(getName(client)); diff --git a/src/lib/server/Server.h b/src/lib/server/Server.h index 352539cbc..58b014a02 100644 --- a/src/lib/server/Server.h +++ b/src/lib/server/Server.h @@ -350,6 +350,8 @@ class Server : public INode void handleClientCloseTimeout(const Event &, void *); void handleSwitchToScreenEvent(const Event &, void *); void handleSwitchInDirectionEvent(const Event &, void *); + void handleTouchActivatedPrimaryEvent(const Event &, void *); + void handleGrabScreenEvent(const Event &, void *); void handleKeyboardBroadcastEvent(const Event &, void *); void handleLockCursorToScreenEvent(const Event &, void *); void handleFakeInputBeginEvent(const Event &, void *); @@ -482,6 +484,11 @@ class Server : public INode bool m_switchTwoTapArmed; SInt32 m_switchTwoTapZone; + // state for touch-triggered screen switching cooldown + // prevents edge-triggered switches from immediately undoing touch switches + Stopwatch m_touchSwitchCooldown; + static constexpr double kTouchSwitchCooldownTime = 0.5; // 500ms cooldown + // modifiers needed before switching bool m_switchNeedsShift; bool m_switchNeedsControl; From ac4b0d44b2bac18c0cbdb611b916a2192dba135c Mon Sep 17 00:00:00 2001 From: Stefan Verleysen Date: Wed, 11 Feb 2026 14:02:11 -0500 Subject: [PATCH 02/25] =?UTF-8?q?refactor:=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20protocol=20versioning,=20platform=20abstraction,=20?= =?UTF-8?q?comment=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create ClientProxy1_9 for kMsgCGrabScreen (was incorrectly in ClientProxy1_0, breaking backward compatibility with older clients) - Bump protocol version 1.8 → 1.9 for touch-activated screen switching - Move Windows-specific SetForegroundWindow logic from Server.cpp to MSWindowsScreen::activateWindowAt() via platform abstraction chain - Remove dead mouseDown/mouseUp calls (PrimaryClient ignores them) - Remove #if WINAPI_MSWINDOWS and Windows.h include from Server.cpp - Clean up comments: keep only "why" comments, remove "what" comments Co-Authored-By: Claude Opus 4.6 --- src/lib/deskflow/IPlatformScreen.h | 9 ++++ src/lib/deskflow/Screen.cpp | 5 ++ src/lib/deskflow/Screen.h | 3 ++ src/lib/deskflow/protocol_types.h | 3 +- src/lib/platform/MSWindowsScreen.cpp | 27 +++++++++++ src/lib/platform/MSWindowsScreen.h | 1 + src/lib/server/ClientProxy1_0.cpp | 22 --------- src/lib/server/ClientProxy1_0.h | 1 - src/lib/server/ClientProxy1_9.cpp | 59 ++++++++++++++++++++++++ src/lib/server/ClientProxy1_9.h | 36 +++++++++++++++ src/lib/server/ClientProxyUnknown.cpp | 5 ++ src/lib/server/PrimaryClient.cpp | 5 ++ src/lib/server/PrimaryClient.h | 3 ++ src/lib/server/Server.cpp | 66 ++++----------------------- 14 files changed, 163 insertions(+), 82 deletions(-) create mode 100644 src/lib/server/ClientProxy1_9.cpp create mode 100644 src/lib/server/ClientProxy1_9.h diff --git a/src/lib/deskflow/IPlatformScreen.h b/src/lib/deskflow/IPlatformScreen.h index 1ca2ceea2..941293523 100644 --- a/src/lib/deskflow/IPlatformScreen.h +++ b/src/lib/deskflow/IPlatformScreen.h @@ -200,6 +200,15 @@ class IPlatformScreen : public IScreen, public IPrimaryScreen, public ISecondary virtual void pollPressedKeys(KeyButtonSet &pressedKeys) const = 0; virtual void clearStaleModifiers() = 0; + //! Activate the window at the given screen coordinates + /*! + Brings the window at position \c x, \c y to the foreground. + Default implementation does nothing; platforms override as needed. + */ + virtual void activateWindowAt(SInt32 x, SInt32 y) + { + } + // Drag-and-drop overrides virtual String &getDraggingFilename() = 0; virtual void clearDraggingFilename() = 0; diff --git a/src/lib/deskflow/Screen.cpp b/src/lib/deskflow/Screen.cpp index f3581201c..8af861e84 100644 --- a/src/lib/deskflow/Screen.cpp +++ b/src/lib/deskflow/Screen.cpp @@ -488,6 +488,11 @@ void Screen::leaveSecondary() m_screen->fakeAllKeysUp(); } +void Screen::activateWindowAt(SInt32 x, SInt32 y) +{ + m_screen->activateWindowAt(x, y); +} + String Screen::getSecureInputApp() const { return m_screen->getSecureInputApp(); diff --git a/src/lib/deskflow/Screen.h b/src/lib/deskflow/Screen.h index 9e4c3ef63..00ea491d8 100644 --- a/src/lib/deskflow/Screen.h +++ b/src/lib/deskflow/Screen.h @@ -236,6 +236,9 @@ class Screen : public IScreen void setEnableDragDrop(bool enabled); + //! Activate the window at the given screen coordinates + void activateWindowAt(SInt32 x, SInt32 y); + //! Determine the name of the app causing a secure input state /*! On MacOS check which app causes a secure input state to be enabled. No diff --git a/src/lib/deskflow/protocol_types.h b/src/lib/deskflow/protocol_types.h index 6b8e609fd..d158a0f89 100644 --- a/src/lib/deskflow/protocol_types.h +++ b/src/lib/deskflow/protocol_types.h @@ -31,9 +31,10 @@ // 1.6: adds clipboard streaming // 1.7 adds security input notifications // 1.8 adds language synchronization functionality +// 1.9 adds touch-activated screen switching // NOTE: with new version, deskflow minor version should increment static const SInt16 kProtocolMajorVersion = 1; -static const SInt16 kProtocolMinorVersion = 8; +static const SInt16 kProtocolMinorVersion = 9; // default contact port number static const UInt16 kDefaultPort = 24800; diff --git a/src/lib/platform/MSWindowsScreen.cpp b/src/lib/platform/MSWindowsScreen.cpp index 46be497bf..6211e5296 100644 --- a/src/lib/platform/MSWindowsScreen.cpp +++ b/src/lib/platform/MSWindowsScreen.cpp @@ -2002,6 +2002,33 @@ String MSWindowsScreen::getSecureInputApp() const return ""; } +void MSWindowsScreen::activateWindowAt(SInt32 x, SInt32 y) +{ + POINT pt = {x, y}; + HWND hwnd = WindowFromPoint(pt); + if (hwnd == NULL) { + return; + } + + HWND root = GetAncestor(hwnd, GA_ROOT); + if (root == NULL) { + return; + } + + // Windows restricts SetForegroundWindow to prevent focus stealing, + // so we use AttachThreadInput to bypass the restriction. + DWORD foreThread = GetWindowThreadProcessId(GetForegroundWindow(), NULL); + DWORD curThread = GetCurrentThreadId(); + if (foreThread != curThread) { + AttachThreadInput(foreThread, curThread, TRUE); + } + SetForegroundWindow(root); + if (foreThread != curThread) { + AttachThreadInput(foreThread, curThread, FALSE); + } + LOG((CLOG_DEBUG1 "touch: forced foreground window 0x%08x", root)); +} + bool MSWindowsScreen::isModifierRepeat(KeyModifierMask oldState, KeyModifierMask state, WPARAM wParam) const { bool result = false; diff --git a/src/lib/platform/MSWindowsScreen.h b/src/lib/platform/MSWindowsScreen.h index 9c98089ee..7bb9889a4 100644 --- a/src/lib/platform/MSWindowsScreen.h +++ b/src/lib/platform/MSWindowsScreen.h @@ -137,6 +137,7 @@ class MSWindowsScreen : public PlatformScreen virtual String &getDraggingFilename(); virtual const String &getDropTarget() const; String getSecureInputApp() const override; + void activateWindowAt(SInt32 x, SInt32 y) override; protected: // IPlatformScreen overrides diff --git a/src/lib/server/ClientProxy1_0.cpp b/src/lib/server/ClientProxy1_0.cpp index 7d1deef57..31b51020f 100644 --- a/src/lib/server/ClientProxy1_0.cpp +++ b/src/lib/server/ClientProxy1_0.cpp @@ -21,7 +21,6 @@ #include "base/IEventQueue.h" #include "base/Log.h" #include "base/TMethodEventJob.h" -#include "deskflow/IPrimaryScreen.h" #include "deskflow/ProtocolUtil.h" #include "deskflow/XDeskflow.h" #include "io/IStream.h" @@ -189,31 +188,10 @@ bool ClientProxy1_0::parseMessage(const UInt8 *code) return recvGrabClipboard(); } else if (memcmp(code, kMsgDClipboard, 4) == 0) { return recvClipboard(); - } else if (memcmp(code, kMsgCGrabScreen, 4) == 0) { - return recvGrabScreen(); } return false; } -bool ClientProxy1_0::recvGrabScreen() -{ - // parse message - SInt16 x, y; - if (!ProtocolUtil::readf(getStream(), kMsgCGrabScreen + 4, &x, &y)) { - return false; - } - LOG((CLOG_DEBUG "received client \"%s\" grab screen request at %d,%d", getName().c_str(), x, y)); - - // notify server to switch to this client - m_events->addEvent(Event( - m_events->forClientProxy().grabScreen(), - getEventTarget(), - IPrimaryScreen::MotionInfo::alloc(x, y) - )); - - return true; -} - void ClientProxy1_0::handleDisconnect(const Event &, void *) { LOG((CLOG_NOTE "client \"%s\" has disconnected", getName().c_str())); diff --git a/src/lib/server/ClientProxy1_0.h b/src/lib/server/ClientProxy1_0.h index bfa6e4e71..0339b44e6 100644 --- a/src/lib/server/ClientProxy1_0.h +++ b/src/lib/server/ClientProxy1_0.h @@ -87,7 +87,6 @@ class ClientProxy1_0 : public ClientProxy bool recvInfo(); bool recvGrabClipboard(); - bool recvGrabScreen(); protected: struct ClientClipboard diff --git a/src/lib/server/ClientProxy1_9.cpp b/src/lib/server/ClientProxy1_9.cpp new file mode 100644 index 000000000..8b0171f79 --- /dev/null +++ b/src/lib/server/ClientProxy1_9.cpp @@ -0,0 +1,59 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "server/ClientProxy1_9.h" + +#include "base/IEventQueue.h" +#include "base/Log.h" +#include "deskflow/IPrimaryScreen.h" +#include "deskflow/ProtocolUtil.h" +#include "deskflow/protocol_types.h" + +#include + +ClientProxy1_9::ClientProxy1_9( + const String &name, deskflow::IStream *adoptedStream, Server *server, IEventQueue *events +) + : ClientProxy1_8(name, adoptedStream, server, events), + m_events(events) +{ +} + +bool ClientProxy1_9::parseMessage(const UInt8 *code) +{ + if (memcmp(code, kMsgCGrabScreen, 4) == 0) { + return recvGrabScreen(); + } + return ClientProxy1_8::parseMessage(code); +} + +bool ClientProxy1_9::recvGrabScreen() +{ + SInt16 x, y; + if (!ProtocolUtil::readf(getStream(), kMsgCGrabScreen + 4, &x, &y)) { + return false; + } + LOG((CLOG_DEBUG "received client \"%s\" grab screen request at %d,%d", getName().c_str(), x, y)); + + m_events->addEvent(Event( + m_events->forClientProxy().grabScreen(), + getEventTarget(), + IPrimaryScreen::MotionInfo::alloc(x, y) + )); + + return true; +} diff --git a/src/lib/server/ClientProxy1_9.h b/src/lib/server/ClientProxy1_9.h new file mode 100644 index 000000000..8f2ed6c21 --- /dev/null +++ b/src/lib/server/ClientProxy1_9.h @@ -0,0 +1,36 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "server/ClientProxy1_8.h" + +//! Proxy for client implementing protocol version 1.9 +class ClientProxy1_9 : public ClientProxy1_8 +{ +public: + ClientProxy1_9(const String &name, deskflow::IStream *adoptedStream, Server *server, IEventQueue *events); + ~ClientProxy1_9() override = default; + +protected: + bool parseMessage(const UInt8 *code) override; + +private: + bool recvGrabScreen(); + + IEventQueue *m_events; +}; diff --git a/src/lib/server/ClientProxyUnknown.cpp b/src/lib/server/ClientProxyUnknown.cpp index 36b1bf586..14c923c95 100644 --- a/src/lib/server/ClientProxyUnknown.cpp +++ b/src/lib/server/ClientProxyUnknown.cpp @@ -36,6 +36,7 @@ #include "server/ClientProxy1_6.h" #include "server/ClientProxy1_7.h" #include "server/ClientProxy1_8.h" +#include "server/ClientProxy1_9.h" #include "server/Server.h" #include @@ -199,6 +200,10 @@ void ClientProxyUnknown::initProxy(const String &name, int major, int minor) case 8: m_proxy = new ClientProxy1_8(name, m_stream, m_server, m_events); break; + + case 9: + m_proxy = new ClientProxy1_9(name, m_stream, m_server, m_events); + break; } } diff --git a/src/lib/server/PrimaryClient.cpp b/src/lib/server/PrimaryClient.cpp index a7024daec..070d4cbbf 100644 --- a/src/lib/server/PrimaryClient.cpp +++ b/src/lib/server/PrimaryClient.cpp @@ -71,6 +71,11 @@ void PrimaryClient::fakeInputEnd() } } +void PrimaryClient::activateWindowAt(SInt32 x, SInt32 y) +{ + m_screen->activateWindowAt(x, y); +} + SInt32 PrimaryClient::getJumpZoneSize() const { return m_screen->getJumpZoneSize(); diff --git a/src/lib/server/PrimaryClient.h b/src/lib/server/PrimaryClient.h index dfab74c7f..965a0c6e4 100644 --- a/src/lib/server/PrimaryClient.h +++ b/src/lib/server/PrimaryClient.h @@ -83,6 +83,9 @@ class PrimaryClient : public BaseClientProxy */ void fakeInputEnd(); + //! Activate the window at the given screen coordinates + void activateWindowAt(SInt32 x, SInt32 y); + //@} //! @name accessors //@{ diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index a8651360a..710db4038 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -47,11 +47,6 @@ #include #include -#if WINAPI_MSWINDOWS -#define WIN32_LEAN_AND_MEAN -#include -#endif - using namespace deskflow::server; // @@ -787,8 +782,7 @@ bool Server::isSwitchOkay( return false; } - // check if we're in touch switch cooldown period - // this prevents edge-triggered switches from immediately undoing + // Cooldown prevents edge-triggered switches from immediately undoing // a touch-triggered switch (which causes rapid bounce switching) if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { LOG((CLOG_DEBUG1 "edge switch blocked by touch cooldown (%.2fs remaining)", @@ -1362,19 +1356,15 @@ void Server::handleTouchActivatedPrimaryEvent(const Event &event, void *) IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); LOG((CLOG_DEBUG1 "touch activated primary at %d,%d", info->m_x, info->m_y)); - // reject if still in cooldown from a recent touch switch if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { LOG((CLOG_DEBUG1 "touch switch rejected (cooldown active)")); return; } if (m_active != m_primaryClient) { - // Save current cursor position on the screen we're leaving - // (same as jumpToScreen does for edge-triggered switches) m_active->setJumpCursorPos(m_x, m_y); - // Clamp touch coordinates away from screen edges to avoid landing - // in the jump zone, which would trigger an immediate edge switch + // Clamp away from jump zones to avoid triggering an immediate edge switch SInt32 x = info->m_x; SInt32 y = info->m_y; SInt32 dx, dy, dw, dh; @@ -1385,45 +1375,13 @@ void Server::handleTouchActivatedPrimaryEvent(const Event &event, void *) y = (std::max)(y, dy + z); y = (std::min)(y, dy + dh - 1 - z); - // Switch back to primary screen at clamped touch position switchScreen(m_primaryClient, x, y, false); - // Synthesize a click at the touch position to focus the target window. - // The hook eats the original touch event to prevent edge detection race, - // so without this the window under the touch point never receives focus. - m_primaryClient->mouseDown(kButtonLeft); - m_primaryClient->mouseUp(kButtonLeft); - - // Force the window under the touch point to the foreground. - // The hook eats the original touch event, so the target window - // never receives the click. We must explicitly activate it. - // Windows restricts SetForegroundWindow to prevent focus stealing, - // so we use AttachThreadInput to bypass the restriction. -#if WINAPI_MSWINDOWS - { - POINT pt = { x, y }; - HWND hwnd = WindowFromPoint(pt); - if (hwnd != NULL) { - HWND root = GetAncestor(hwnd, GA_ROOT); - if (root != NULL) { - DWORD foreThread = GetWindowThreadProcessId( - GetForegroundWindow(), NULL); - DWORD curThread = GetCurrentThreadId(); - if (foreThread != curThread) { - AttachThreadInput(foreThread, curThread, TRUE); - } - SetForegroundWindow(root); - if (foreThread != curThread) { - AttachThreadInput(foreThread, curThread, FALSE); - } - LOG((CLOG_DEBUG1 "touch: forced foreground window 0x%08x", root)); - } - } - } -#endif + // The hook eats the original touch event, so the window under the + // touch point never receives it. Explicitly activate that window. + m_primaryClient->activateWindowAt(x, y); - // Start cooldown to prevent edge-triggered switches from immediately - // undoing this touch-triggered switch + // Cooldown prevents edge-triggered switches from undoing this touch switch m_touchSwitchCooldown.reset(); LOG((CLOG_DEBUG1 "touch switch cooldown started")); } @@ -1436,21 +1394,15 @@ void Server::handleGrabScreenEvent(const Event &event, void *vclient) LOG((CLOG_DEBUG1 "client \"%s\" requests grab at %d,%d", getName(client).c_str(), info->m_x, info->m_y)); - // reject if still in cooldown from a recent touch switch if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { LOG((CLOG_DEBUG1 "grab rejected (cooldown active)")); return; } if (client != m_active) { - // Save current cursor position on the screen we're leaving - // (same as jumpToScreen does for edge-triggered switches) m_active->setJumpCursorPos(m_x, m_y); - // Clamp touch coordinates away from screen edges to avoid landing - // in the jump zone on the primary screen (which would trigger an - // immediate edge switch back). Only matters when switching away - // from primary, since jump zones only exist on primary. + // Clamp away from jump zones to avoid triggering an immediate edge switch SInt32 x = info->m_x; SInt32 y = info->m_y; SInt32 dx, dy, dw, dh; @@ -1461,11 +1413,9 @@ void Server::handleGrabScreenEvent(const Event &event, void *vclient) y = (std::max)(y, dy + z); y = (std::min)(y, dy + dh - 1 - z); - // Switch to the requesting client at clamped touch position switchScreen(client, x, y, false); - // Start cooldown to prevent edge-triggered switches from immediately - // undoing this touch-triggered switch + // Cooldown prevents edge-triggered switches from undoing this touch switch m_touchSwitchCooldown.reset(); LOG((CLOG_DEBUG1 "touch switch cooldown started")); } From e83302caccebf0cb441f0bb38f7247f3975637e4 Mon Sep 17 00:00:00 2001 From: Stefan Verleysen Date: Mon, 16 Feb 2026 22:28:59 -0500 Subject: [PATCH 03/25] refactor: address PR review round 2 - Rename grabScreen -> grabInput (avoid "screen" ambiguity) - Strip "what" comments, keep only "why" explanations - Remove Win7/Win8 backwards-compat guards (direct WM_POINTER/GetPointerType) - Fix cursor hiding on secondary touchscreen clients (WM_MOUSEMOVE hider dismissal) - Update copyright dates to 2012-2026 - Drop compat table entry, clean up empty virtuals and doxygen blocks --- .gitignore | 3 + src/gui/CMakeLists.txt | 2 - src/lib/base/EventTypes.cpp | 6 +- src/lib/base/EventTypes.h | 31 +++------- src/lib/client/Client.cpp | 25 +++++--- src/lib/client/Client.h | 7 ++- src/lib/client/ServerProxy.cpp | 8 +-- src/lib/client/ServerProxy.h | 9 +-- src/lib/deskflow/IPlatformScreen.h | 11 +--- src/lib/deskflow/Screen.h | 3 +- src/lib/deskflow/protocol_types.cpp | 4 +- src/lib/deskflow/protocol_types.h | 10 +-- src/lib/platform/MSWindowsDesks.cpp | 93 ++++++++++++++++++++-------- src/lib/platform/MSWindowsDesks.h | 5 +- src/lib/platform/MSWindowsHook.cpp | 7 +-- src/lib/platform/MSWindowsScreen.cpp | 77 +++++++---------------- src/lib/platform/MSWindowsScreen.h | 6 +- src/lib/platform/dfwhook.h | 10 +-- src/lib/server/ClientProxy1_9.cpp | 14 ++--- src/lib/server/ClientProxy1_9.h | 5 +- src/lib/server/PrimaryClient.h | 3 +- src/lib/server/Server.cpp | 30 +++------ src/lib/server/Server.h | 7 +-- 23 files changed, 169 insertions(+), 207 deletions(-) diff --git a/.gitignore b/.gitignore index 1723cb2a9..816292151 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ deskflow-config.toml /scripts/*.egg-info /*.user *.ui.autosave + +# local strategy docs and tracking (not for upstream) +/local/ diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index b042bcad4..0064323e2 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -41,9 +41,7 @@ include_directories(./src) # gui library autogen headers: # qt doesn't seem to auto include the autogen headers for libraries. -# single-config generators (Make) use include/, multi-config (VS) use include_$/ include_directories(${PROJECT_BINARY_DIR}/src/lib/gui/gui_autogen/include) -include_directories(${PROJECT_BINARY_DIR}/src/lib/gui/gui_autogen/include_$) # generated includes include_directories(${PROJECT_BINARY_DIR}/config) diff --git a/src/lib/base/EventTypes.cpp b/src/lib/base/EventTypes.cpp index 85896a3c7..93c97d05e 100644 --- a/src/lib/base/EventTypes.cpp +++ b/src/lib/base/EventTypes.cpp @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2013-2016 Symless Ltd. + * Copyright (C) 2013-2026 Symless Ltd. * * This package is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -115,7 +115,7 @@ REGISTER_EVENT(ClientListener, connected) REGISTER_EVENT(ClientProxy, ready) REGISTER_EVENT(ClientProxy, disconnected) -REGISTER_EVENT(ClientProxy, grabScreen) +REGISTER_EVENT(ClientProxy, grabInput) // // ClientProxyUnknown @@ -178,7 +178,7 @@ REGISTER_EVENT(IScreen, error) REGISTER_EVENT(IScreen, shapeChanged) REGISTER_EVENT(IScreen, suspend) REGISTER_EVENT(IScreen, resume) -REGISTER_EVENT(IScreen, grabScreen) +REGISTER_EVENT(IScreen, grabInput) // // IpcServer diff --git a/src/lib/base/EventTypes.h b/src/lib/base/EventTypes.h index 8885f1692..aee96a73c 100644 --- a/src/lib/base/EventTypes.h +++ b/src/lib/base/EventTypes.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2013-2016 Symless Ltd. + * Copyright (C) 2013-2026 Symless Ltd. * * This package is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -388,7 +388,7 @@ class ClientListenerEvents : public EventTypes class ClientProxyEvents : public EventTypes { public: - ClientProxyEvents() : m_ready(Event::kUnknown), m_disconnected(Event::kUnknown), m_grabScreen(Event::kUnknown) + ClientProxyEvents() : m_ready(Event::kUnknown), m_disconnected(Event::kUnknown), m_grabInput(Event::kUnknown) { } @@ -410,20 +410,14 @@ class ClientProxyEvents : public EventTypes */ Event::Type disconnected(); - //! Get grab screen event type - /*! - Returns the grab screen event type. This is sent when a client - requests to become the active screen (e.g., due to touch input). - Event data is MotionInfo* with the position where activation occurred. - */ - Event::Type grabScreen(); + Event::Type grabInput(); //@} private: Event::Type m_ready; Event::Type m_disconnected; - Event::Type m_grabScreen; + Event::Type m_grabInput; }; class ClientProxyUnknownEvents : public EventTypes @@ -660,11 +654,6 @@ class IPrimaryScreenEvents : public EventTypes //! end of fake input event type Event::Type fakeInputEnd(); - //! touch activated primary screen event type - /*! - Event data is MotionInfo* with the position where touch occurred. - This is sent when touch input on the primary screen should activate it. - */ Event::Type touchActivatedPrimary(); //@} @@ -692,7 +681,7 @@ class IScreenEvents : public EventTypes m_shapeChanged(Event::kUnknown), m_suspend(Event::kUnknown), m_resume(Event::kUnknown), - m_grabScreen(Event::kUnknown) + m_grabInput(Event::kUnknown) { } @@ -727,13 +716,7 @@ class IScreenEvents : public EventTypes */ Event::Type resume(); - //! Get grab screen event type - /*! - Returns the grab screen event type. This is sent when a secondary screen - requests to become the active screen (e.g., due to touch input). - Event data is MotionInfo* with the position where activation occurred. - */ - Event::Type grabScreen(); + Event::Type grabInput(); //@} @@ -742,7 +725,7 @@ class IScreenEvents : public EventTypes Event::Type m_shapeChanged; Event::Type m_suspend; Event::Type m_resume; - Event::Type m_grabScreen; + Event::Type m_grabInput; }; class ClipboardEvents : public EventTypes diff --git a/src/lib/client/Client.cpp b/src/lib/client/Client.cpp index b45c107ae..5cddd8dbb 100644 --- a/src/lib/client/Client.cpp +++ b/src/lib/client/Client.cpp @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -89,8 +89,8 @@ Client::Client( m_events->forIScreen().resume(), getEventTarget(), new TMethodEventJob(this, &Client::handleResume) ); m_events->adoptHandler( - m_events->forIScreen().grabScreen(), m_screen->getEventTarget(), - new TMethodEventJob(this, &Client::handleGrabScreen) + m_events->forIScreen().grabInput(), m_screen->getEventTarget(), + new TMethodEventJob(this, &Client::handleGrabInput) ); if (m_args.m_enableDragDrop) { @@ -112,7 +112,7 @@ Client::~Client() m_events->removeHandler(m_events->forIScreen().suspend(), getEventTarget()); m_events->removeHandler(m_events->forIScreen().resume(), getEventTarget()); - m_events->removeHandler(m_events->forIScreen().grabScreen(), m_screen->getEventTarget()); + m_events->removeHandler(m_events->forIScreen().grabInput(), m_screen->getEventTarget()); cleanupTimer(); cleanupScreen(); @@ -246,6 +246,13 @@ void Client::enter(SInt32 xAbs, SInt32 yAbs, UInt32, KeyModifierMask mask, bool) m_screen->mouseMove(xAbs, yAbs); m_screen->enter(mask); + if (m_pendingTouchActivation) { + m_pendingTouchActivation = false; + m_screen->activateWindowAt(m_touchActivateX, m_touchActivateY); + m_screen->mouseDown(kButtonLeft); + m_screen->mouseUp(kButtonLeft); + } + if (m_sendFileThread) { StreamChunker::interruptFile(); m_sendFileThread.reset(nullptr); @@ -725,13 +732,15 @@ void Client::handleResume(const Event &, void *) } } -void Client::handleGrabScreen(const Event &event, void *) +void Client::handleGrabInput(const Event &event, void *) { - // Forward grab screen request to server via protocol IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); if (m_server != NULL) { - LOG((CLOG_DEBUG1 "requesting screen grab at %d,%d", info->m_x, info->m_y)); - m_server->grabScreen(info->m_x, info->m_y); + LOG((CLOG_DEBUG1 "requesting grab input at %d,%d", info->m_x, info->m_y)); + m_pendingTouchActivation = true; + m_touchActivateX = info->m_x; + m_touchActivateY = info->m_y; + m_server->grabInput(info->m_x, info->m_y); } } diff --git a/src/lib/client/Client.h b/src/lib/client/Client.h index a80b2f7b9..217d744a1 100644 --- a/src/lib/client/Client.h +++ b/src/lib/client/Client.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -220,7 +220,7 @@ class Client : public IClient, public INode void handleHello(const Event &, void *); void handleSuspend(const Event &event, void *); void handleResume(const Event &event, void *); - void handleGrabScreen(const Event &event, void *); + void handleGrabInput(const Event &event, void *); void handleFileChunkSending(const Event &, void *); void handleFileRecieveCompleted(const Event &, void *); void handleStopRetry(const Event &, void *); @@ -260,4 +260,7 @@ class Client : public IClient, public INode size_t m_maximumClipboardSize; deskflow::ClientArgs m_args; size_t m_resolvedAddressesCount = 0; + bool m_pendingTouchActivation = false; + SInt32 m_touchActivateX = 0; + SInt32 m_touchActivateY = 0; }; diff --git a/src/lib/client/ServerProxy.cpp b/src/lib/client/ServerProxy.cpp index e57a6e1c4..d7feb6891 100644 --- a/src/lib/client/ServerProxy.cpp +++ b/src/lib/client/ServerProxy.cpp @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -372,10 +372,10 @@ bool ServerProxy::onGrabClipboard(ClipboardID id) return true; } -void ServerProxy::grabScreen(SInt32 x, SInt32 y) +void ServerProxy::grabInput(SInt32 x, SInt32 y) { - LOG((CLOG_DEBUG1 "requesting screen grab at %d,%d", x, y)); - ProtocolUtil::writef(m_stream, kMsgCGrabScreen, x, y); + LOG((CLOG_DEBUG1 "requesting grab input at %d,%d", x, y)); + ProtocolUtil::writef(m_stream, kMsgCGrabInput, x, y); } void ServerProxy::onClipboardChanged(ClipboardID id, const IClipboard *clipboard) diff --git a/src/lib/client/ServerProxy.h b/src/lib/client/ServerProxy.h index c02816e3e..185c264b9 100644 --- a/src/lib/client/ServerProxy.h +++ b/src/lib/client/ServerProxy.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -61,12 +61,7 @@ class ServerProxy bool onGrabClipboard(ClipboardID); void onClipboardChanged(ClipboardID, const IClipboard *); - //! Request to grab screen - /*! - Sends a request to the server to make this client the active screen. - This is typically called when touch input is detected on the client. - */ - void grabScreen(SInt32 x, SInt32 y); + void grabInput(SInt32 x, SInt32 y); //@} diff --git a/src/lib/deskflow/IPlatformScreen.h b/src/lib/deskflow/IPlatformScreen.h index 941293523..b5d9c2407 100644 --- a/src/lib/deskflow/IPlatformScreen.h +++ b/src/lib/deskflow/IPlatformScreen.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -200,14 +200,7 @@ class IPlatformScreen : public IScreen, public IPrimaryScreen, public ISecondary virtual void pollPressedKeys(KeyButtonSet &pressedKeys) const = 0; virtual void clearStaleModifiers() = 0; - //! Activate the window at the given screen coordinates - /*! - Brings the window at position \c x, \c y to the foreground. - Default implementation does nothing; platforms override as needed. - */ - virtual void activateWindowAt(SInt32 x, SInt32 y) - { - } + virtual void activateWindowAt(SInt32 x, SInt32 y) { /* do nothing */ } // Drag-and-drop overrides virtual String &getDraggingFilename() = 0; diff --git a/src/lib/deskflow/Screen.h b/src/lib/deskflow/Screen.h index 00ea491d8..628ba87bc 100644 --- a/src/lib/deskflow/Screen.h +++ b/src/lib/deskflow/Screen.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -236,7 +236,6 @@ class Screen : public IScreen void setEnableDragDrop(bool enabled); - //! Activate the window at the given screen coordinates void activateWindowAt(SInt32 x, SInt32 y); //! Determine the name of the app causing a secure input state diff --git a/src/lib/deskflow/protocol_types.cpp b/src/lib/deskflow/protocol_types.cpp index fbe6a4116..920393bf2 100644 --- a/src/lib/deskflow/protocol_types.cpp +++ b/src/lib/deskflow/protocol_types.cpp @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -50,7 +50,7 @@ const char *const kMsgDFileTransfer = "DFTR%1i%s"; const char *const kMsgDDragInfo = "DDRG%2i%s"; const char *const kMsgDSecureInputNotification = "SECN%s"; const char *const kMsgDLanguageSynchronisation = "LSYN%s"; -const char *const kMsgCGrabScreen = "CGRB%2i%2i"; +const char *const kMsgCGrabInput = "CGRB%2i%2i"; const char *const kMsgQInfo = "QINF"; const char *const kMsgEIncompatible = "EICV%2i%2i"; const char *const kMsgEBusy = "EBSY"; diff --git a/src/lib/deskflow/protocol_types.h b/src/lib/deskflow/protocol_types.h index d158a0f89..2d312a910 100644 --- a/src/lib/deskflow/protocol_types.h +++ b/src/lib/deskflow/protocol_types.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -31,7 +31,7 @@ // 1.6: adds clipboard streaming // 1.7 adds security input notifications // 1.8 adds language synchronization functionality -// 1.9 adds touch-activated screen switching +// 1.9 adds touch-activated input switching // NOTE: with new version, deskflow minor version should increment static const SInt16 kProtocolMajorVersion = 1; static const SInt16 kProtocolMinorVersion = 9; @@ -295,10 +295,10 @@ extern const char *const kMsgDSecureInputNotification; // $1 = List of server languages extern const char *const kMsgDLanguageSynchronisation; -// grab screen request: secondary -> primary -// Client requests to become the active screen (e.g., due to touch input). +// grab input request: secondary -> primary +// Client requests to become the active computer (e.g., due to touch input). // $1 = x position, $2 = y position where activation occurred -extern const char *const kMsgCGrabScreen; +extern const char *const kMsgCGrabInput; // // query codes diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index 85e4e35e6..ec3e1793f 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2004 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -32,6 +32,7 @@ #include "platform/dfwhook.h" #include +#include // these are only defined when WINVER >= 0x0500 #if !defined(SPI_GETMOUSESPEED) @@ -123,6 +124,7 @@ MSWindowsDesks::MSWindowsDesks( : m_isPrimary(isPrimary), m_noHooks(noHooks), m_isOnScreen(m_isPrimary), + m_deskLeaveTime(0), m_x(0), m_y(0), m_w(0), @@ -409,17 +411,52 @@ void MSWindowsDesks::destroyWindow(HWND hwnd) const LRESULT CALLBACK MSWindowsDesks::primaryDeskProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + switch (msg) { + case WM_SETCURSOR: + SetCursor(NULL); + return TRUE; + } return DefWindowProc(hwnd, msg, wParam, lParam); } LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { - case WM_SETCURSOR: - // Force blank cursor. On touchscreen devices, ShowCursor(FALSE) may - // not reliably hide the cursor, so we also set a NULL cursor here. - SetCursor(NULL); - return TRUE; + case WM_POINTERDOWN: { + MSWindowsDesks *self = reinterpret_cast( + GetWindowLongPtr(hwnd, GWLP_USERDATA)); + if (self) { + UINT32 pointerId = GET_POINTERID_WPARAM(wParam); + DWORD pointerType = PT_POINTER; + GetPointerType(pointerId, &pointerType); + if (pointerType == PT_TOUCH || pointerType == PT_PEN) { + SInt32 x = GET_X_LPARAM(lParam); + SInt32 y = GET_Y_LPARAM(lParam); + LOG((CLOG_DEBUG1 "secondary WM_POINTERDOWN touch at %d,%d", x, y)); + PostThreadMessage(self->m_threadID, DESKFLOW_MSG_TOUCH, + static_cast(x), static_cast(y)); + return 0; + } + } + break; + } + + case WM_MOUSEMOVE: { + MSWindowsDesks *self = reinterpret_cast( + GetWindowLongPtr(hwnd, GWLP_USERDATA)); + if (self && IsWindowVisible(hwnd)) { + if (GetTickCount64() - self->m_deskLeaveTime < 200) { + break; + } + ReleaseCapture(); + SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + HCURSOR arrow = LoadCursor(NULL, IDC_ARROW); + SetClassLongPtr(hwnd, GCLP_HCURSOR, reinterpret_cast(arrow)); + SetCursor(arrow); + } + break; + } } return DefWindowProc(hwnd, msg, wParam, lParam); @@ -512,14 +549,18 @@ void MSWindowsDesks::deskEnter(Desk *desk) if (!m_isPrimary) { ReleaseCapture(); - // Restore WS_EX_TRANSPARENT that was removed in deskLeave LONG_PTR exStyle = GetWindowLongPtr(desk->m_window, GWL_EXSTYLE); + // let hit-testing pass through to windows underneath SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle | WS_EX_TRANSPARENT); } + SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + setCursorVisibility(true); - SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + HCURSOR arrow = LoadCursor(NULL, IDC_ARROW); + SetClassLongPtr(desk->m_window, GCLP_HCURSOR, reinterpret_cast(arrow)); + SetCursor(arrow); // restore the foreground window // XXX -- this raises the window to the top of the Z-order. we @@ -546,8 +587,9 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) // active window. int x, y, w, h; if (desk->m_lowLevel) { - // with a low level hook the cursor will never budge so - // just a 1x1 window is sufficient. + // LL hook keeps the cursor pinned at center, so 1x1 is enough. + // primaryDeskProc handles WM_SETCURSOR to force a blank cursor + // (ShowCursor(FALSE) is unreliable on touchscreen devices). x = m_xCenter; y = m_yCenter; w = 1; @@ -563,6 +605,10 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) } SetWindowPos(desk->m_window, HWND_TOP, x, y, w, h, SWP_NOACTIVATE | SWP_SHOWWINDOW); + // WM_SETCURSOR won't fire until the cursor moves, so force the + // blank cursor immediately (LL hook pins cursor at center, no movement). + SetCursor(NULL); + // switch to requested keyboard layout ActivateKeyboardLayout(keyLayout, 0); @@ -596,23 +642,20 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) } } } else { - // Remove WS_EX_TRANSPARENT so the hider window receives hit-testing - // and its blank cursor class applies. Without this, hit-testing - // passes through and the cursor of the window behind is shown. + EnableWindow(desk->m_window, TRUE); + + SetClassLongPtr(desk->m_window, GCLP_HCURSOR, reinterpret_cast(m_cursor)); + LONG_PTR exStyle = GetWindowLongPtr(desk->m_window, GWL_EXSTYLE); SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle & ~WS_EX_TRANSPARENT); - // Cover the entire screen with the hider window so the blank cursor - // class applies everywhere. On touchscreen devices, ShowCursor(FALSE) - // is unreliable, so the blank cursor on a full-screen window is the - // primary hiding mechanism. SetWindowPos(desk->m_window, HWND_TOPMOST, m_x, m_y, m_w, m_h, SWP_NOACTIVATE | SWP_SHOWWINDOW); SetCapture(desk->m_window); - // Brief delay for cursor hiding to take effect, then center the cursor. - LOG_DEBUG1("centering cursor on leave: %+d,%+d", m_xCenter, m_yCenter); + // brief delay for the hider window's blank cursor to take effect ARCH->sleep(0.03); + m_deskLeaveTime = GetTickCount64(); deskMouseMove(m_xCenter, m_yCenter); } } @@ -633,12 +676,9 @@ void MSWindowsDesks::deskThread(void *vdesk) // create a window. we use this window to hide the cursor. try { desk->m_window = createWindow(m_deskClass, DESKFLOW_APP_NAME "Desk"); + SetWindowLongPtr(desk->m_window, GWLP_USERDATA, reinterpret_cast(this)); LOG((CLOG_DEBUG "desk %s window is 0x%08x", desk->m_name.c_str(), desk->m_window)); - // Register for raw touch input on the desk window. This MUST be on - // the desk thread (not the main thread) because the main event loop - // uses QS_ALLPOSTMESSAGE which never wakes for WM_INPUT messages. - // The desk thread's GetMessage(NULL,0,0) has no such filter. RAWINPUTDEVICE rids[4] = {}; rids[0].usUsagePage = 0x0D; rids[0].usUsage = 0x04; // Touch Screen rids[0].dwFlags = RIDEV_INPUTSINK; rids[0].hwndTarget = desk->m_window; @@ -652,7 +692,6 @@ void MSWindowsDesks::deskThread(void *vdesk) LOG((CLOG_DEBUG "desk %s: registered raw touch input on desk window", desk->m_name.c_str())); } else { - // fallback: try just touch screen if (RegisterRawInputDevices(rids, 1, sizeof(RAWINPUTDEVICE))) { LOG((CLOG_DEBUG "desk %s: registered touch screen raw input", desk->m_name.c_str())); @@ -682,8 +721,6 @@ void MSWindowsDesks::deskThread(void *vdesk) continue; case WM_INPUT: { - // Raw touch input from digitizer. Forward to main thread as - // DESKFLOW_MSG_TOUCH for the same handling as the LL mouse hook. UINT size = 0; GetRawInputData( reinterpret_cast(msg.lParam), RID_INPUT, @@ -695,10 +732,12 @@ void MSWindowsDesks::deskThread(void *vdesk) buffer, &size, sizeof(RAWINPUTHEADER)) != static_cast(-1)) { RAWINPUT *raw = reinterpret_cast(buffer); if (raw->header.dwType == RIM_TYPEHID && - raw->data.hid.dwCount > 0 && raw->data.hid.dwSizeHid > 0) { + raw->data.hid.dwCount > 0 && raw->data.hid.dwSizeHid > 0 && + m_isPrimary) { POINT pt; GetCursorPos(&pt); LOG((CLOG_DEBUG1 "desk raw touch at %d,%d", pt.x, pt.y)); + // same handling path as the LL mouse hook touch detection PostThreadMessage(m_threadID, DESKFLOW_MSG_TOUCH, static_cast(pt.x), static_cast(pt.y)); } diff --git a/src/lib/platform/MSWindowsDesks.h b/src/lib/platform/MSWindowsDesks.h index eb225a381..76dd54822 100644 --- a/src/lib/platform/MSWindowsDesks.h +++ b/src/lib/platform/MSWindowsDesks.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2004 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -252,6 +252,9 @@ class MSWindowsDesks // true if mouse has entered the screen bool m_isOnScreen; + // suppress WM_MOUSEMOVE hider dismissal briefly after deskLeave + ULONGLONG m_deskLeaveTime; + // our resources ATOM m_deskClass; HCURSOR m_cursor; diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index e042fab41..837cd5781 100644 --- a/src/lib/platform/MSWindowsHook.cpp +++ b/src/lib/platform/MSWindowsHook.cpp @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2011 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -597,8 +597,7 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) // decode the message MSLLHOOKSTRUCT *info = reinterpret_cast(lParam); - // detect touch-originated mouse events via dwExtraInfo signature. - // this must run before the injected check, because Windows marks + // must run before the injected check: Windows marks // touch-synthesized mouse events as injected (LLMHF_INJECTED). if (g_touchActivateScreen) { bool isTouchEvent = (info->dwExtraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE; @@ -609,7 +608,7 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) // On primary: eat the event to prevent edge detection and // button-state locking (isLockedToScreen) from racing. // On secondary (client): let it through so the click reaches - // the target window (e.g. Start menu) — no jump zones on clients. + // the target window (e.g. Start menu); no jump zones on clients. if (g_isPrimary) { return 1; } diff --git a/src/lib/platform/MSWindowsScreen.cpp b/src/lib/platform/MSWindowsScreen.cpp index 6211e5296..2d93cb085 100644 --- a/src/lib/platform/MSWindowsScreen.cpp +++ b/src/lib/platform/MSWindowsScreen.cpp @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -44,6 +44,7 @@ #include #include #include +#include // suppress warning about GetVersionEx, which is used indirectly in this // compilation unit. @@ -84,28 +85,6 @@ #define PBT_APMRESUMEAUTOMATIC 0x0012 #endif -// WM_POINTER stuff (Windows 8+) -#if !defined(WM_POINTERDOWN) -#define WM_POINTERDOWN 0x0246 -#define WM_POINTERUP 0x0247 -#define WM_POINTERUPDATE 0x0245 -#define WM_POINTERENTER 0x0249 -#define WM_POINTERLEAVE 0x024A -#define GET_POINTERID_WPARAM(wParam) (LOWORD(wParam)) -#endif - -#if !defined(PT_POINTER) -#define PT_POINTER 1 -#define PT_TOUCH 2 -#define PT_PEN 3 -#define PT_MOUSE 4 -#endif - -// Function pointer type for GetPointerType (loaded dynamically for Win7 compat) -typedef BOOL(WINAPI *GetPointerTypeFunc)(UINT32 pointerId, DWORD *pointerType); -static GetPointerTypeFunc s_getPointerType = NULL; -static bool s_pointerApiChecked = false; - // // MSWindowsScreen // @@ -504,7 +483,6 @@ void MSWindowsScreen::setOptions(const OptionsList &options) { m_desks->setOptions(options); - // check for touch input local option for (UInt32 i = 0, n = (UInt32)options.size(); i < n; i += 2) { if (options[i] == kOptionTouchActivateScreen) { m_touchActivateScreen = (options[i + 1] != 0); @@ -995,8 +973,6 @@ bool MSWindowsScreen::onPreDispatch(HWND hwnd, UINT message, WPARAM wParam, LPAR return true; case DESKFLOW_MSG_TOUCH: - // Thread messages (PostThreadMessage) bypass DispatchMessage, so - // this must be handled here in onPreDispatch, not in onEvent. if (!m_touchActivateScreen || m_isOnScreen) return true; { @@ -1007,12 +983,12 @@ bool MSWindowsScreen::onPreDispatch(HWND hwnd, UINT message, WPARAM wParam, LPAR SInt32 x = static_cast(wParam); SInt32 y = static_cast(lParam); if (m_isPrimary) { - LOG((CLOG_INFO "hook: touch activating primary screen at %d,%d", x, y)); + LOG((CLOG_INFO "hook: touch activating primary at %d,%d", x, y)); sendEvent(m_events->forIPrimaryScreen().touchActivatedPrimary(), MotionInfo::alloc(x, y)); } else { - LOG((CLOG_INFO "hook: touch requesting screen grab at %d,%d", x, y)); - sendEvent(m_events->forIScreen().grabScreen(), + LOG((CLOG_INFO "hook: touch requesting grab input at %d,%d", x, y)); + sendEvent(m_events->forIScreen().grabInput(), MotionInfo::alloc(x, y)); } } @@ -1107,11 +1083,9 @@ bool MSWindowsScreen::onEvent(HWND, UINT msg, WPARAM wParam, LPARAM lParam, LRES case WM_POINTERUP: case WM_POINTERUPDATE: if (onPointerInput(wParam, lParam)) { - // Touch input was consumed (kept local or triggered screen switch) *result = 0; return true; } - // Fall through to let DefWindowProc convert to mouse messages return false; /* On windows 10 we don't receive WM_POWERBROADCAST after sleep. @@ -1481,22 +1455,8 @@ bool MSWindowsScreen::onScreensaver(bool activated) bool MSWindowsScreen::isPointerTypeTouch(UINT32 pointerId) const { - // Dynamically load GetPointerType for Windows 7 compatibility - if (!s_pointerApiChecked) { - s_pointerApiChecked = true; - HMODULE user32 = GetModuleHandle("user32.dll"); - if (user32 != NULL) { - s_getPointerType = (GetPointerTypeFunc)GetProcAddress(user32, "GetPointerType"); - } - } - - if (s_getPointerType == NULL) { - // API not available (Windows 7 or earlier) - return false; - } - DWORD pointerType = PT_POINTER; - if (s_getPointerType(pointerId, &pointerType)) { + if (GetPointerType(pointerId, &pointerType)) { return (pointerType == PT_TOUCH || pointerType == PT_PEN); } return false; @@ -1517,16 +1477,16 @@ bool MSWindowsScreen::onPointerInput(WPARAM wParam, LPARAM lParam) m_touchDebounceTimer.reset(); POINT pt; - if (!GetCursorPos(&pt)) - return false; + pt.x = GET_X_LPARAM(lParam); + pt.y = GET_Y_LPARAM(lParam); if (m_isPrimary) { - LOG((CLOG_INFO "touch activating primary screen at %d,%d", pt.x, pt.y)); + LOG((CLOG_INFO "touch activating primary at %d,%d", pt.x, pt.y)); sendEvent(m_events->forIPrimaryScreen().touchActivatedPrimary(), MotionInfo::alloc(pt.x, pt.y)); } else { - LOG((CLOG_INFO "touch requesting screen grab at %d,%d", pt.x, pt.y)); - sendEvent(m_events->forIScreen().grabScreen(), + LOG((CLOG_INFO "touch requesting grab input at %d,%d", pt.x, pt.y)); + sendEvent(m_events->forIScreen().grabInput(), MotionInfo::alloc(pt.x, pt.y)); } @@ -2017,16 +1977,21 @@ void MSWindowsScreen::activateWindowAt(SInt32 x, SInt32 y) // Windows restricts SetForegroundWindow to prevent focus stealing, // so we use AttachThreadInput to bypass the restriction. - DWORD foreThread = GetWindowThreadProcessId(GetForegroundWindow(), NULL); + HWND foreground = GetForegroundWindow(); + DWORD foreThread = 0; + if (foreground != NULL) { + foreThread = GetWindowThreadProcessId(foreground, NULL); + } DWORD curThread = GetCurrentThreadId(); - if (foreThread != curThread) { - AttachThreadInput(foreThread, curThread, TRUE); + BOOL attached = FALSE; + if (foreThread != 0 && foreThread != curThread) { + attached = AttachThreadInput(foreThread, curThread, TRUE); } SetForegroundWindow(root); - if (foreThread != curThread) { + if (attached) { AttachThreadInput(foreThread, curThread, FALSE); } - LOG((CLOG_DEBUG1 "touch: forced foreground window 0x%08x", root)); + LOG((CLOG_DEBUG1 "touch: forced foreground window %p", static_cast(root))); } bool MSWindowsScreen::isModifierRepeat(KeyModifierMask oldState, KeyModifierMask state, WPARAM wParam) const diff --git a/src/lib/platform/MSWindowsScreen.h b/src/lib/platform/MSWindowsScreen.h index 7bb9889a4..91fcb0739 100644 --- a/src/lib/platform/MSWindowsScreen.h +++ b/src/lib/platform/MSWindowsScreen.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -362,10 +362,8 @@ class MSWindowsScreen : public PlatformScreen PrimaryKeyDownList m_primaryKeyDownList; MSWindowsPowerManager m_powerManager; - // When true, touching this screen activates it (switches focus here) bool m_touchActivateScreen; - // Debounce rapid touch events to prevent multiple switch requests Stopwatch m_touchDebounceTimer; - static constexpr double kTouchDebounceTime = 0.15; // 150ms debounce + static constexpr double kTouchDebounceTime = 0.15; }; diff --git a/src/lib/platform/dfwhook.h b/src/lib/platform/dfwhook.h index 7280ac5fb..0e8db3c39 100644 --- a/src/lib/platform/dfwhook.h +++ b/src/lib/platform/dfwhook.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -18,14 +18,6 @@ #pragma once -// hack: vs2005 doesn't declare _WIN32_WINNT, so we need to hard code it. -// however, some say that this should be hard coded since it defines the -// target system, but since this is suposed to compile on pre-XP, maybe -// we should just leave it like this. -#if _MSC_VER == 1400 -#define _WIN32_WINNT 0x0400 -#endif - #include "base/EventTypes.h" #define WIN32_LEAN_AND_MEAN diff --git a/src/lib/server/ClientProxy1_9.cpp b/src/lib/server/ClientProxy1_9.cpp index 8b0171f79..6adb48ac2 100644 --- a/src/lib/server/ClientProxy1_9.cpp +++ b/src/lib/server/ClientProxy1_9.cpp @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * * This package is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -35,22 +35,22 @@ ClientProxy1_9::ClientProxy1_9( bool ClientProxy1_9::parseMessage(const UInt8 *code) { - if (memcmp(code, kMsgCGrabScreen, 4) == 0) { - return recvGrabScreen(); + if (memcmp(code, kMsgCGrabInput, 4) == 0) { + return recvGrabInput(); } return ClientProxy1_8::parseMessage(code); } -bool ClientProxy1_9::recvGrabScreen() +bool ClientProxy1_9::recvGrabInput() { SInt16 x, y; - if (!ProtocolUtil::readf(getStream(), kMsgCGrabScreen + 4, &x, &y)) { + if (!ProtocolUtil::readf(getStream(), kMsgCGrabInput + 4, &x, &y)) { return false; } - LOG((CLOG_DEBUG "received client \"%s\" grab screen request at %d,%d", getName().c_str(), x, y)); + LOG((CLOG_DEBUG "received client \"%s\" grab input request at %d,%d", getName().c_str(), x, y)); m_events->addEvent(Event( - m_events->forClientProxy().grabScreen(), + m_events->forClientProxy().grabInput(), getEventTarget(), IPrimaryScreen::MotionInfo::alloc(x, y) )); diff --git a/src/lib/server/ClientProxy1_9.h b/src/lib/server/ClientProxy1_9.h index 8f2ed6c21..6c4590210 100644 --- a/src/lib/server/ClientProxy1_9.h +++ b/src/lib/server/ClientProxy1_9.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * * This package is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -19,7 +19,6 @@ #include "server/ClientProxy1_8.h" -//! Proxy for client implementing protocol version 1.9 class ClientProxy1_9 : public ClientProxy1_8 { public: @@ -30,7 +29,7 @@ class ClientProxy1_9 : public ClientProxy1_8 bool parseMessage(const UInt8 *code) override; private: - bool recvGrabScreen(); + bool recvGrabInput(); IEventQueue *m_events; }; diff --git a/src/lib/server/PrimaryClient.h b/src/lib/server/PrimaryClient.h index 965a0c6e4..8203eab15 100644 --- a/src/lib/server/PrimaryClient.h +++ b/src/lib/server/PrimaryClient.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -83,7 +83,6 @@ class PrimaryClient : public BaseClientProxy */ void fakeInputEnd(); - //! Activate the window at the given screen coordinates void activateWindowAt(SInt32 x, SInt32 y); //@} diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index 710db4038..057054d2c 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -782,11 +782,10 @@ bool Server::isSwitchOkay( return false; } - // Cooldown prevents edge-triggered switches from immediately undoing - // a touch-triggered switch (which causes rapid bounce switching) - if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { + double elapsedTouchCooldown = m_touchSwitchCooldown.getTime(); + if (elapsedTouchCooldown > 0.0 && elapsedTouchCooldown < kTouchSwitchCooldownTime) { LOG((CLOG_DEBUG1 "edge switch blocked by touch cooldown (%.2fs remaining)", - kTouchSwitchCooldownTime - m_touchSwitchCooldown.getTime())); + kTouchSwitchCooldownTime - elapsedTouchCooldown)); return false; } @@ -1356,11 +1355,6 @@ void Server::handleTouchActivatedPrimaryEvent(const Event &event, void *) IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); LOG((CLOG_DEBUG1 "touch activated primary at %d,%d", info->m_x, info->m_y)); - if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { - LOG((CLOG_DEBUG1 "touch switch rejected (cooldown active)")); - return; - } - if (m_active != m_primaryClient) { m_active->setJumpCursorPos(m_x, m_y); @@ -1381,28 +1375,21 @@ void Server::handleTouchActivatedPrimaryEvent(const Event &event, void *) // touch point never receives it. Explicitly activate that window. m_primaryClient->activateWindowAt(x, y); - // Cooldown prevents edge-triggered switches from undoing this touch switch m_touchSwitchCooldown.reset(); LOG((CLOG_DEBUG1 "touch switch cooldown started")); } } -void Server::handleGrabScreenEvent(const Event &event, void *vclient) +void Server::handleGrabInputEvent(const Event &event, void *vclient) { IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); BaseClientProxy *client = static_cast(vclient); LOG((CLOG_DEBUG1 "client \"%s\" requests grab at %d,%d", getName(client).c_str(), info->m_x, info->m_y)); - if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { - LOG((CLOG_DEBUG1 "grab rejected (cooldown active)")); - return; - } - if (client != m_active) { m_active->setJumpCursorPos(m_x, m_y); - // Clamp away from jump zones to avoid triggering an immediate edge switch SInt32 x = info->m_x; SInt32 y = info->m_y; SInt32 dx, dy, dw, dh; @@ -1415,7 +1402,6 @@ void Server::handleGrabScreenEvent(const Event &event, void *vclient) switchScreen(client, x, y, false); - // Cooldown prevents edge-triggered switches from undoing this touch switch m_touchSwitchCooldown.reset(); LOG((CLOG_DEBUG1 "touch switch cooldown started")); } @@ -2074,8 +2060,8 @@ bool Server::addClient(BaseClientProxy *client) new TMethodEventJob(this, &Server::handleClipboardChanged, client) ); m_events->adoptHandler( - m_events->forClientProxy().grabScreen(), client->getEventTarget(), - new TMethodEventJob(this, &Server::handleGrabScreenEvent, client) + m_events->forClientProxy().grabInput(), client->getEventTarget(), + new TMethodEventJob(this, &Server::handleGrabInputEvent, client) ); // add to list @@ -2105,7 +2091,7 @@ bool Server::removeClient(BaseClientProxy *client) m_events->removeHandler(m_events->forIScreen().shapeChanged(), client->getEventTarget()); m_events->removeHandler(m_events->forClipboard().clipboardGrabbed(), client->getEventTarget()); m_events->removeHandler(m_events->forClipboard().clipboardChanged(), client->getEventTarget()); - m_events->removeHandler(m_events->forClientProxy().grabScreen(), client->getEventTarget()); + m_events->removeHandler(m_events->forClientProxy().grabInput(), client->getEventTarget()); // remove from list m_clients.erase(getName(client)); diff --git a/src/lib/server/Server.h b/src/lib/server/Server.h index 58b014a02..d211abb1d 100644 --- a/src/lib/server/Server.h +++ b/src/lib/server/Server.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -351,7 +351,7 @@ class Server : public INode void handleSwitchToScreenEvent(const Event &, void *); void handleSwitchInDirectionEvent(const Event &, void *); void handleTouchActivatedPrimaryEvent(const Event &, void *); - void handleGrabScreenEvent(const Event &, void *); + void handleGrabInputEvent(const Event &, void *); void handleKeyboardBroadcastEvent(const Event &, void *); void handleLockCursorToScreenEvent(const Event &, void *); void handleFakeInputBeginEvent(const Event &, void *); @@ -484,10 +484,9 @@ class Server : public INode bool m_switchTwoTapArmed; SInt32 m_switchTwoTapZone; - // state for touch-triggered screen switching cooldown // prevents edge-triggered switches from immediately undoing touch switches Stopwatch m_touchSwitchCooldown; - static constexpr double kTouchSwitchCooldownTime = 0.5; // 500ms cooldown + static constexpr double kTouchSwitchCooldownTime = 0.5; // modifiers needed before switching bool m_switchNeedsShift; From 32e4cdb2e70713767629e76be14fb1c4cf56c07f Mon Sep 17 00:00:00 2001 From: Stefan Verleysen Date: Fri, 20 Feb 2026 09:23:29 -0500 Subject: [PATCH 04/25] fix: resolve touch click bugs on secondary (client) screens - Save foreground window in secondary deskLeave so deskEnter can restore it; fixes SetForegroundWindow failing on re-enter which required a double-tap to click after switching - Skip hider auto-hide on touch-generated WM_MOUSEMOVE by checking the MI_WP_SIGNATURE touch signature via GetMessageExtraInfo - Restrict WM_INPUT raw touch handler to primary screens; on clients GetCursorPos returns the parked center position, not the touch point, causing clicks to land at screen center - Only eat touch events in the LL hook when in relay mode, not watch mode; fixes touch on server's secondary touchscreens acting as a giant cursor on the primary monitor - Move TOUCH_SIGNATURE defines to dfwhook.h shared header - Add diagnostic logging to activateWindowAt and click replay path Co-Authored-By: Claude Opus 4.6 --- src/lib/client/Client.cpp | 2 ++ src/lib/platform/MSWindowsDesks.cpp | 11 +++++++---- src/lib/platform/MSWindowsHook.cpp | 16 +++++----------- src/lib/platform/MSWindowsScreen.cpp | 20 ++++++++++++++++---- src/lib/platform/dfwhook.h | 5 +++++ 5 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/lib/client/Client.cpp b/src/lib/client/Client.cpp index 5cddd8dbb..3d63c87ab 100644 --- a/src/lib/client/Client.cpp +++ b/src/lib/client/Client.cpp @@ -248,6 +248,8 @@ void Client::enter(SInt32 xAbs, SInt32 yAbs, UInt32, KeyModifierMask mask, bool) if (m_pendingTouchActivation) { m_pendingTouchActivation = false; + LOG((CLOG_DEBUG1 "touch: replaying click at %d,%d (cursor at %d,%d)", + m_touchActivateX, m_touchActivateY, xAbs, yAbs)); m_screen->activateWindowAt(m_touchActivateX, m_touchActivateY); m_screen->mouseDown(kButtonLeft); m_screen->mouseUp(kButtonLeft); diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index ec3e1793f..6ef0d56e7 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -442,6 +442,10 @@ LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM w } case WM_MOUSEMOVE: { + LPARAM extraInfo = GetMessageExtraInfo(); + if ((extraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE) + break; + MSWindowsDesks *self = reinterpret_cast( GetWindowLongPtr(hwnd, GWLP_USERDATA)); if (self && IsWindowVisible(hwnd)) { @@ -642,6 +646,7 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) } } } else { + desk->m_foregroundWindow = getForegroundWindow(); EnableWindow(desk->m_window, TRUE); SetClassLongPtr(desk->m_window, GCLP_HCURSOR, reinterpret_cast(m_cursor)); @@ -731,13 +736,11 @@ void MSWindowsDesks::deskThread(void *vdesk) reinterpret_cast(msg.lParam), RID_INPUT, buffer, &size, sizeof(RAWINPUTHEADER)) != static_cast(-1)) { RAWINPUT *raw = reinterpret_cast(buffer); - if (raw->header.dwType == RIM_TYPEHID && - raw->data.hid.dwCount > 0 && raw->data.hid.dwSizeHid > 0 && - m_isPrimary) { + if (raw->header.dwType == RIM_TYPEHID && m_isPrimary && + raw->data.hid.dwCount > 0 && raw->data.hid.dwSizeHid > 0) { POINT pt; GetCursorPos(&pt); LOG((CLOG_DEBUG1 "desk raw touch at %d,%d", pt.x, pt.y)); - // same handling path as the LL mouse hook touch detection PostThreadMessage(m_threadID, DESKFLOW_MSG_TOUCH, static_cast(pt.x), static_cast(pt.y)); } diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index 837cd5781..eacd9729a 100644 --- a/src/lib/platform/MSWindowsHook.cpp +++ b/src/lib/platform/MSWindowsHook.cpp @@ -46,12 +46,6 @@ static bool g_fakeServerInput = false; static BOOL g_isPrimary = TRUE; static bool g_touchActivateScreen = false; -// Microsoft touch signature in dwExtraInfo (MI_WP_SIGNATURE). -// The upper 24 bits (masked by 0xFFFFFF00) identify touch-generated -// mouse events; the lower 8 bits contain pen/touch flags. -#define TOUCH_SIGNATURE_MASK 0xFFFFFF00 -#define TOUCH_SIGNATURE 0xFF515700 - MSWindowsHook::MSWindowsHook() { } @@ -605,11 +599,11 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) SInt32 x = static_cast(info->pt.x); SInt32 y = static_cast(info->pt.y); PostThreadMessage(g_threadID, DESKFLOW_MSG_TOUCH, x, y); - // On primary: eat the event to prevent edge detection and - // button-state locking (isLockedToScreen) from racing. - // On secondary (client): let it through so the click reaches - // the target window (e.g. Start menu); no jump zones on clients. - if (g_isPrimary) { + // Only eat in relay mode (cursor has left this screen) to + // prevent edge detection and isLockedToScreen from racing. + // In watch mode (cursor on server), let it through for + // normal touch behavior on the server's own screens. + if (g_isPrimary && g_mode == kHOOK_RELAY_EVENTS) { return 1; } } diff --git a/src/lib/platform/MSWindowsScreen.cpp b/src/lib/platform/MSWindowsScreen.cpp index 2d93cb085..e8ded171d 100644 --- a/src/lib/platform/MSWindowsScreen.cpp +++ b/src/lib/platform/MSWindowsScreen.cpp @@ -1967,17 +1967,22 @@ void MSWindowsScreen::activateWindowAt(SInt32 x, SInt32 y) POINT pt = {x, y}; HWND hwnd = WindowFromPoint(pt); if (hwnd == NULL) { + LOG((CLOG_DEBUG1 "touch: no window at %d,%d", x, y)); return; } HWND root = GetAncestor(hwnd, GA_ROOT); if (root == NULL) { + LOG((CLOG_DEBUG1 "touch: no root ancestor for window %p", static_cast(hwnd))); return; } - // Windows restricts SetForegroundWindow to prevent focus stealing, - // so we use AttachThreadInput to bypass the restriction. HWND foreground = GetForegroundWindow(); + if (foreground == root) { + LOG((CLOG_DEBUG1 "touch: window %p already foreground", static_cast(root))); + return; + } + DWORD foreThread = 0; if (foreground != NULL) { foreThread = GetWindowThreadProcessId(foreground, NULL); @@ -1987,11 +1992,18 @@ void MSWindowsScreen::activateWindowAt(SInt32 x, SInt32 y) if (foreThread != 0 && foreThread != curThread) { attached = AttachThreadInput(foreThread, curThread, TRUE); } - SetForegroundWindow(root); + BOOL ok = SetForegroundWindow(root); if (attached) { AttachThreadInput(foreThread, curThread, FALSE); } - LOG((CLOG_DEBUG1 "touch: forced foreground window %p", static_cast(root))); + + if (!ok) { + LOG((CLOG_DEBUG1 "touch: SetForegroundWindow(%p) failed (foreground was %p), " + "click will activate via WM_MOUSEACTIVATE", + static_cast(root), static_cast(foreground))); + } else { + LOG((CLOG_DEBUG1 "touch: activated window %p at %d,%d", static_cast(root), x, y)); + } } bool MSWindowsScreen::isModifierRepeat(KeyModifierMask oldState, KeyModifierMask state, WPARAM wParam) const diff --git a/src/lib/platform/dfwhook.h b/src/lib/platform/dfwhook.h index 0e8db3c39..f3c8ac41f 100644 --- a/src/lib/platform/dfwhook.h +++ b/src/lib/platform/dfwhook.h @@ -46,6 +46,11 @@ #define DESKFLOW_HOOK_FAKE_INPUT_VIRTUAL_KEY VK_CANCEL #define DESKFLOW_HOOK_FAKE_INPUT_SCANCODE 0 +// Microsoft touch signature in dwExtraInfo (MI_WP_SIGNATURE). +// Touch-synthesized mouse events carry this in the upper 24 bits. +#define TOUCH_SIGNATURE_MASK 0xFFFFFF00 +#define TOUCH_SIGNATURE 0xFF515700 + extern "C" { From b14df002a37bc2b24bbf16ee8a2475d0695974b9 Mon Sep 17 00:00:00 2001 From: Stefan Verleysen Date: Mon, 2 Mar 2026 10:32:10 -0500 Subject: [PATCH 05/25] fix: server-side touch interference and client touch click replay Server: skip LL hook touch handling in watch mode to prevent double-tap requirement on the server's own touchscreen. Register raw input for touch only when cursor leaves (deskLeave) and unregister on return (deskEnter). Replace GetCursorPos with proper HID preparsed data parsing for accurate touch coordinates. Client: replay consumed first-touch as InjectTouchInput on the desk thread, with mouse_event fallback for processes without UIAccess. Handle WM_POINTERACTIVATE with PA_NOACTIVATE to prevent the hider window from stealing focus on touch. Add 100ms settle delay after deskEnter before replay so full-screen apps can resume input processing. Plumbing: add fakeTouchClick virtual method through IPlatformScreen/Screen/MSWindowsScreen to route touch replay through the platform layer. Link hid.lib on Windows for HID preparsed data APIs. Co-Authored-By: Claude Opus 4.6 --- src/lib/client/Client.cpp | 4 +- src/lib/deskflow/IPlatformScreen.h | 1 + src/lib/deskflow/Screen.cpp | 5 + src/lib/deskflow/Screen.h | 2 + src/lib/platform/CMakeLists.txt | 4 + src/lib/platform/MSWindowsDesks.cpp | 260 ++++++++++++++++++++++++--- src/lib/platform/MSWindowsDesks.h | 21 +++ src/lib/platform/MSWindowsHook.cpp | 8 +- src/lib/platform/MSWindowsScreen.cpp | 5 + src/lib/platform/MSWindowsScreen.h | 1 + 10 files changed, 275 insertions(+), 36 deletions(-) diff --git a/src/lib/client/Client.cpp b/src/lib/client/Client.cpp index 3d63c87ab..a295a239a 100644 --- a/src/lib/client/Client.cpp +++ b/src/lib/client/Client.cpp @@ -248,11 +248,11 @@ void Client::enter(SInt32 xAbs, SInt32 yAbs, UInt32, KeyModifierMask mask, bool) if (m_pendingTouchActivation) { m_pendingTouchActivation = false; + ARCH->sleep(0.1); LOG((CLOG_DEBUG1 "touch: replaying click at %d,%d (cursor at %d,%d)", m_touchActivateX, m_touchActivateY, xAbs, yAbs)); m_screen->activateWindowAt(m_touchActivateX, m_touchActivateY); - m_screen->mouseDown(kButtonLeft); - m_screen->mouseUp(kButtonLeft); + m_screen->fakeTouchClick(m_touchActivateX, m_touchActivateY); } if (m_sendFileThread) { diff --git a/src/lib/deskflow/IPlatformScreen.h b/src/lib/deskflow/IPlatformScreen.h index b5d9c2407..cf816cca0 100644 --- a/src/lib/deskflow/IPlatformScreen.h +++ b/src/lib/deskflow/IPlatformScreen.h @@ -201,6 +201,7 @@ class IPlatformScreen : public IScreen, public IPrimaryScreen, public ISecondary virtual void clearStaleModifiers() = 0; virtual void activateWindowAt(SInt32 x, SInt32 y) { /* do nothing */ } + virtual void fakeTouchClick(SInt32 x, SInt32 y) { /* do nothing */ } // Drag-and-drop overrides virtual String &getDraggingFilename() = 0; diff --git a/src/lib/deskflow/Screen.cpp b/src/lib/deskflow/Screen.cpp index 8af861e84..ae858ed35 100644 --- a/src/lib/deskflow/Screen.cpp +++ b/src/lib/deskflow/Screen.cpp @@ -249,6 +249,11 @@ void Screen::mouseWheel(SInt32 xDelta, SInt32 yDelta) const m_screen->fakeMouseWheel(xDelta, yDelta); } +void Screen::fakeTouchClick(SInt32 x, SInt32 y) +{ + m_screen->fakeTouchClick(x, y); +} + void Screen::resetOptions() { // reset options diff --git a/src/lib/deskflow/Screen.h b/src/lib/deskflow/Screen.h index 628ba87bc..495776990 100644 --- a/src/lib/deskflow/Screen.h +++ b/src/lib/deskflow/Screen.h @@ -150,6 +150,8 @@ class Screen : public IScreen /*! Synthesize mouse events to generate a press of mouse button \c id. */ + void fakeTouchClick(SInt32 x, SInt32 y); + void mouseDown(ButtonID id); //! Notify of mouse release diff --git a/src/lib/platform/CMakeLists.txt b/src/lib/platform/CMakeLists.txt index 778d6feb2..71cf36d16 100644 --- a/src/lib/platform/CMakeLists.txt +++ b/src/lib/platform/CMakeLists.txt @@ -67,6 +67,10 @@ include_directories(${inc}) add_library(platform STATIC ${sources}) target_link_libraries(platform client ${libs}) +if(WIN32) + target_link_libraries(platform hid) +endif() + macro(link_wayland_libs) target_link_libraries(platform ${LIBXKBCOMMON_LINK_LIBRARIES} ${GLIB2_LINK_LIBRARIES} ${LIBM_LIBRARIES}) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index 6ef0d56e7..907750939 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -34,6 +34,12 @@ #include #include +#ifndef _NTDEF_ +typedef LONG NTSTATUS; +#endif +#include +#include + // these are only defined when WINVER >= 0x0500 #if !defined(SPI_GETMOUSESPEED) #define SPI_GETMOUSESPEED 112 @@ -87,6 +93,8 @@ #define DESKFLOW_MSG_FAKE_REL_MOVE DESKFLOW_HOOK_LAST_MSG + 11 // enable; #define DESKFLOW_MSG_FAKE_INPUT DESKFLOW_HOOK_LAST_MSG + 12 +// x; y +#define DESKFLOW_MSG_FAKE_TOUCH DESKFLOW_HOOK_LAST_MSG + 13 static void send_keyboard_input(WORD wVk, WORD wScan, DWORD dwFlags) { @@ -318,6 +326,11 @@ void MSWindowsDesks::fakeMouseButton(ButtonID button, bool press) sendMessage(DESKFLOW_MSG_FAKE_BUTTON, flags, data); } +void MSWindowsDesks::fakeTouchClick(SInt32 x, SInt32 y) const +{ + sendMessage(DESKFLOW_MSG_FAKE_TOUCH, static_cast(x), static_cast(y)); +} + void MSWindowsDesks::fakeMouseMove(SInt32 x, SInt32 y) const { sendMessage(DESKFLOW_MSG_FAKE_MOVE, static_cast(x), static_cast(y)); @@ -422,6 +435,9 @@ LRESULT CALLBACK MSWindowsDesks::primaryDeskProc(HWND hwnd, UINT msg, WPARAM wPa LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { + case WM_POINTERACTIVATE: + return PA_NOACTIVATE; + case WM_POINTERDOWN: { MSWindowsDesks *self = reinterpret_cast( GetWindowLongPtr(hwnd, GWLP_USERDATA)); @@ -479,6 +495,65 @@ void MSWindowsDesks::deskMouseMove(SInt32 x, SInt32 y) const ); } +void MSWindowsDesks::deskFakeTouchClick(SInt32 x, SInt32 y) const +{ + static bool s_touchReady = false; + + if (!s_touchReady) { + if (!InitializeTouchInjection(1, TOUCH_FEEDBACK_DEFAULT)) { + DWORD err = GetLastError(); + LOG((CLOG_WARN "touch: InitializeTouchInjection failed, err=%d", err)); + deskMouseMove(x, y); + send_mouse_input(MOUSEEVENTF_LEFTDOWN, 0, 0, 0); + send_mouse_input(MOUSEEVENTF_LEFTUP, 0, 0, 0); + return; + } + LOG((CLOG_DEBUG1 "touch: InitializeTouchInjection succeeded")); + s_touchReady = true; + } + + POINTER_TOUCH_INFO contact; + memset(&contact, 0, sizeof(POINTER_TOUCH_INFO)); + + contact.pointerInfo.pointerType = PT_TOUCH; + contact.pointerInfo.pointerId = 0; + contact.pointerInfo.ptPixelLocation.x = x; + contact.pointerInfo.ptPixelLocation.y = y; + + contact.touchFlags = TOUCH_FLAG_NONE; + contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE; + contact.orientation = 90; + contact.pressure = 32000; + contact.rcContact.left = x - 2; + contact.rcContact.right = x + 2; + contact.rcContact.top = y - 2; + contact.rcContact.bottom = y + 2; + + contact.pointerInfo.pointerFlags = + POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT; + + LOG((CLOG_DEBUG1 "touch: injecting DOWN at %d,%d", x, y)); + + bool injected = false; + if (InjectTouchInput(1, &contact)) { + Sleep(30); + contact.pointerInfo.pointerFlags = POINTER_FLAG_UP; + InjectTouchInput(1, &contact); + injected = true; + LOG((CLOG_DEBUG1 "touch: injected touch at %d,%d", x, y)); + } else { + DWORD err = GetLastError(); + LOG((CLOG_WARN "touch: InjectTouchInput failed at %d,%d, err=%d", x, y, err)); + } + + Sleep(50); + + deskMouseMove(x, y); + send_mouse_input(MOUSEEVENTF_LEFTDOWN, 0, 0, 0); + send_mouse_input(MOUSEEVENTF_LEFTUP, 0, 0, 0); + LOG((CLOG_DEBUG1 "touch: sent mouse click at %d,%d (touch injected=%d)", x, y, injected)); +} + void MSWindowsDesks::deskMouseRelativeMove(SInt32 dx, SInt32 dy) const { // relative moves are subject to cursor acceleration which we don't @@ -556,6 +631,8 @@ void MSWindowsDesks::deskEnter(Desk *desk) LONG_PTR exStyle = GetWindowLongPtr(desk->m_window, GWL_EXSTYLE); // let hit-testing pass through to windows underneath SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle | WS_EX_TRANSPARENT); + } else { + registerTouchRawInput(desk->m_window, false); } SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); @@ -645,6 +722,8 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) AttachThreadInput(thatThread, thisThread, FALSE); } } + + registerTouchRawInput(desk->m_window, true); } else { desk->m_foregroundWindow = getForegroundWindow(); EnableWindow(desk->m_window, TRUE); @@ -665,6 +744,142 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) } } +MSWindowsDesks::HidTouchDevice MSWindowsDesks::initHidTouchDevice(HANDLE hDevice) +{ + HidTouchDevice dev = {}; + dev.valid = false; + + UINT ppSize = 0; + if (GetRawInputDeviceInfo(hDevice, RIDI_PREPARSEDDATA, NULL, &ppSize) != 0 || + ppSize == 0) { + return dev; + } + + dev.preparsedData.resize(ppSize); + if (GetRawInputDeviceInfo(hDevice, RIDI_PREPARSEDDATA, + dev.preparsedData.data(), &ppSize) == static_cast(-1)) { + return dev; + } + + auto pp = reinterpret_cast(dev.preparsedData.data()); + + HIDP_CAPS caps = {}; + if (HidP_GetCaps(pp, &caps) != HIDP_STATUS_SUCCESS) { + return dev; + } + + std::vector valCaps(caps.NumberInputValueCaps); + USHORT numValCaps = caps.NumberInputValueCaps; + if (numValCaps == 0 || + HidP_GetValueCaps(HidP_Input, valCaps.data(), &numValCaps, pp) != HIDP_STATUS_SUCCESS) { + return dev; + } + + // find a link collection that has both X and Y on the generic desktop page + struct CollectionInfo { + bool hasX = false; + bool hasY = false; + LONG maxX = 0; + LONG maxY = 0; + }; + std::unordered_map collections; + + for (USHORT i = 0; i < numValCaps; ++i) { + const auto &vc = valCaps[i]; + if (vc.UsagePage != HID_USAGE_PAGE_GENERIC) + continue; + + USAGE usage = vc.IsRange ? vc.Range.UsageMin : vc.NotRange.Usage; + auto &ci = collections[vc.LinkCollection]; + if (usage == HID_USAGE_GENERIC_X) { + ci.hasX = true; + ci.maxX = vc.LogicalMax > 0 ? vc.LogicalMax : vc.PhysicalMax; + } else if (usage == HID_USAGE_GENERIC_Y) { + ci.hasY = true; + ci.maxY = vc.LogicalMax > 0 ? vc.LogicalMax : vc.PhysicalMax; + } + } + + for (const auto &pair : collections) { + if (pair.second.hasX && pair.second.hasY && + pair.second.maxX > 0 && pair.second.maxY > 0) { + dev.linkCollection = pair.first; + dev.logicalMaxX = pair.second.maxX; + dev.logicalMaxY = pair.second.maxY; + dev.valid = true; + LOG((CLOG_DEBUG "HID touch device: linkCollection=%d logicalMax X=%ld Y=%ld", + dev.linkCollection, dev.logicalMaxX, dev.logicalMaxY)); + break; + } + } + + return dev; +} + +bool MSWindowsDesks::parseHidTouch(const RAWINPUT *raw, const HidTouchDevice &dev, + SInt32 &outX, SInt32 &outY) +{ + if (!dev.valid) + return false; + + auto pp = reinterpret_cast( + const_cast(dev.preparsedData.data())); + auto report = const_cast( + reinterpret_cast(raw->data.hid.bRawData)); + ULONG reportLen = raw->data.hid.dwSizeHid; + + // check Tip Switch (digitizer page 0x0D, usage 0x42) + USAGE usages[16] = {}; + ULONG numUsages = 16; + if (HidP_GetUsages(HidP_Input, 0x0D, dev.linkCollection, + usages, &numUsages, pp, + report, reportLen) != HIDP_STATUS_SUCCESS) { + numUsages = 0; + } + + bool tipDown = false; + for (ULONG i = 0; i < numUsages; ++i) { + if (usages[i] == 0x42) { + tipDown = true; + break; + } + } + if (!tipDown) + return false; + + ULONG rawX = 0, rawY = 0; + if (HidP_GetUsageValue(HidP_Input, HID_USAGE_PAGE_GENERIC, + dev.linkCollection, HID_USAGE_GENERIC_X, + &rawX, pp, report, reportLen) != HIDP_STATUS_SUCCESS) { + return false; + } + if (HidP_GetUsageValue(HidP_Input, HID_USAGE_PAGE_GENERIC, + dev.linkCollection, HID_USAGE_GENERIC_Y, + &rawY, pp, report, reportLen) != HIDP_STATUS_SUCCESS) { + return false; + } + + outX = m_x + static_cast(rawX * m_w / dev.logicalMaxX); + outY = m_y + static_cast(rawY * m_h / dev.logicalMaxY); + return true; +} + +void MSWindowsDesks::registerTouchRawInput(HWND window, bool enable) +{ + RAWINPUTDEVICE rids[4] = {}; + DWORD flags = enable ? RIDEV_INPUTSINK : RIDEV_REMOVE; + HWND target = enable ? window : NULL; + + rids[0] = {0x0D, 0x04, flags, target}; + rids[1] = {0x0D, 0x05, flags, target}; + rids[2] = {0x0D, 0x01, flags, target}; + rids[3] = {0x0D, 0x02, flags, target}; + + if (!RegisterRawInputDevices(rids, 4, sizeof(RAWINPUTDEVICE))) { + RegisterRawInputDevices(rids, 1, sizeof(RAWINPUTDEVICE)); + } +} + void MSWindowsDesks::deskThread(void *vdesk) { MSG msg; @@ -683,28 +898,6 @@ void MSWindowsDesks::deskThread(void *vdesk) desk->m_window = createWindow(m_deskClass, DESKFLOW_APP_NAME "Desk"); SetWindowLongPtr(desk->m_window, GWLP_USERDATA, reinterpret_cast(this)); LOG((CLOG_DEBUG "desk %s window is 0x%08x", desk->m_name.c_str(), desk->m_window)); - - RAWINPUTDEVICE rids[4] = {}; - rids[0].usUsagePage = 0x0D; rids[0].usUsage = 0x04; // Touch Screen - rids[0].dwFlags = RIDEV_INPUTSINK; rids[0].hwndTarget = desk->m_window; - rids[1].usUsagePage = 0x0D; rids[1].usUsage = 0x05; // Touch Pad - rids[1].dwFlags = RIDEV_INPUTSINK; rids[1].hwndTarget = desk->m_window; - rids[2].usUsagePage = 0x0D; rids[2].usUsage = 0x01; // Digitizer - rids[2].dwFlags = RIDEV_INPUTSINK; rids[2].hwndTarget = desk->m_window; - rids[3].usUsagePage = 0x0D; rids[3].usUsage = 0x02; // Pen - rids[3].dwFlags = RIDEV_INPUTSINK; rids[3].hwndTarget = desk->m_window; - if (RegisterRawInputDevices(rids, 4, sizeof(RAWINPUTDEVICE))) { - LOG((CLOG_DEBUG "desk %s: registered raw touch input on desk window", - desk->m_name.c_str())); - } else { - if (RegisterRawInputDevices(rids, 1, sizeof(RAWINPUTDEVICE))) { - LOG((CLOG_DEBUG "desk %s: registered touch screen raw input", - desk->m_name.c_str())); - } else { - LOG((CLOG_WARN "desk %s: failed to register raw touch input, error=%d", - desk->m_name.c_str(), GetLastError())); - } - } } catch (...) { // ignore LOG((CLOG_DEBUG "can't create desk window for %s", desk->m_name.c_str())); @@ -736,13 +929,20 @@ void MSWindowsDesks::deskThread(void *vdesk) reinterpret_cast(msg.lParam), RID_INPUT, buffer, &size, sizeof(RAWINPUTHEADER)) != static_cast(-1)) { RAWINPUT *raw = reinterpret_cast(buffer); - if (raw->header.dwType == RIM_TYPEHID && m_isPrimary && + if (raw->header.dwType == RIM_TYPEHID && raw->data.hid.dwCount > 0 && raw->data.hid.dwSizeHid > 0) { - POINT pt; - GetCursorPos(&pt); - LOG((CLOG_DEBUG1 "desk raw touch at %d,%d", pt.x, pt.y)); - PostThreadMessage(m_threadID, DESKFLOW_MSG_TOUCH, - static_cast(pt.x), static_cast(pt.y)); + auto it = m_hidTouchDevices.find(raw->header.hDevice); + if (it == m_hidTouchDevices.end()) { + it = m_hidTouchDevices.emplace( + raw->header.hDevice, initHidTouchDevice(raw->header.hDevice)).first; + } + + SInt32 tx, ty; + if (it->second.valid && parseHidTouch(raw, it->second, tx, ty)) { + LOG((CLOG_DEBUG1 "desk raw touch at %d,%d", tx, ty)); + PostThreadMessage(m_threadID, DESKFLOW_MSG_TOUCH, + static_cast(tx), static_cast(ty)); + } } } } @@ -808,6 +1008,10 @@ void MSWindowsDesks::deskThread(void *vdesk) deskMouseRelativeMove(static_cast(msg.wParam), static_cast(msg.lParam)); break; + case DESKFLOW_MSG_FAKE_TOUCH: + deskFakeTouchClick(static_cast(msg.wParam), static_cast(msg.lParam)); + break; + case DESKFLOW_MSG_FAKE_WHEEL: // XXX -- add support for x-axis scrolling if (msg.lParam != 0) { diff --git a/src/lib/platform/MSWindowsDesks.h b/src/lib/platform/MSWindowsDesks.h index 76dd54822..e8a868583 100644 --- a/src/lib/platform/MSWindowsDesks.h +++ b/src/lib/platform/MSWindowsDesks.h @@ -30,6 +30,9 @@ #define WIN32_LEAN_AND_MEAN #include +#include +#include + class Event; class EventQueueTimer; class Thread; @@ -187,6 +190,8 @@ class MSWindowsDesks */ void fakeMouseWheel(SInt32 xDelta, SInt32 yDelta) const; + void fakeTouchClick(SInt32 x, SInt32 y) const; + //@} private: @@ -204,6 +209,14 @@ class MSWindowsDesks }; typedef std::map Desks; + struct HidTouchDevice { + std::vector preparsedData; + USHORT linkCollection; + LONG logicalMaxX; + LONG logicalMaxY; + bool valid; + }; + // initialization and shutdown operations HCURSOR createBlankCursor() const; void destroyCursor(HCURSOR cursor) const; @@ -214,6 +227,7 @@ class MSWindowsDesks // message handlers void deskMouseMove(SInt32 x, SInt32 y) const; + void deskFakeTouchClick(SInt32 x, SInt32 y) const; void deskMouseRelativeMove(SInt32 dx, SInt32 dy) const; void deskEnter(Desk *desk); void deskLeave(Desk *desk, HKL keyLayout); @@ -238,6 +252,11 @@ class MSWindowsDesks void closeDesktop(HDESK); String getDesktopName(HDESK); + HidTouchDevice initHidTouchDevice(HANDLE hDevice); + bool parseHidTouch(const RAWINPUT *raw, const HidTouchDevice &dev, + SInt32 &outX, SInt32 &outY); + void registerTouchRawInput(HWND window, bool enable); + // our desk window procs static LRESULT CALLBACK primaryDeskProc(HWND, UINT, WPARAM, LPARAM); static LRESULT CALLBACK secondaryDeskProc(HWND, UINT, WPARAM, LPARAM); @@ -295,4 +314,6 @@ class MSWindowsDesks // true if program should stop on desk switch. bool m_stopOnDeskSwitch; + + std::unordered_map m_hidTouchDevices; }; diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index eacd9729a..fa85e41d4 100644 --- a/src/lib/platform/MSWindowsHook.cpp +++ b/src/lib/platform/MSWindowsHook.cpp @@ -593,17 +593,13 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) // must run before the injected check: Windows marks // touch-synthesized mouse events as injected (LLMHF_INJECTED). - if (g_touchActivateScreen) { + if (g_touchActivateScreen && g_mode == kHOOK_RELAY_EVENTS) { bool isTouchEvent = (info->dwExtraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE; if (isTouchEvent && (wParam == WM_LBUTTONDOWN || wParam == WM_MOUSEMOVE)) { SInt32 x = static_cast(info->pt.x); SInt32 y = static_cast(info->pt.y); PostThreadMessage(g_threadID, DESKFLOW_MSG_TOUCH, x, y); - // Only eat in relay mode (cursor has left this screen) to - // prevent edge detection and isLockedToScreen from racing. - // In watch mode (cursor on server), let it through for - // normal touch behavior on the server's own screens. - if (g_isPrimary && g_mode == kHOOK_RELAY_EVENTS) { + if (g_isPrimary) { return 1; } } diff --git a/src/lib/platform/MSWindowsScreen.cpp b/src/lib/platform/MSWindowsScreen.cpp index e8ded171d..d2d60599c 100644 --- a/src/lib/platform/MSWindowsScreen.cpp +++ b/src/lib/platform/MSWindowsScreen.cpp @@ -2006,6 +2006,11 @@ void MSWindowsScreen::activateWindowAt(SInt32 x, SInt32 y) } } +void MSWindowsScreen::fakeTouchClick(SInt32 x, SInt32 y) +{ + m_desks->fakeTouchClick(x, y); +} + bool MSWindowsScreen::isModifierRepeat(KeyModifierMask oldState, KeyModifierMask state, WPARAM wParam) const { bool result = false; diff --git a/src/lib/platform/MSWindowsScreen.h b/src/lib/platform/MSWindowsScreen.h index 91fcb0739..6aa31816f 100644 --- a/src/lib/platform/MSWindowsScreen.h +++ b/src/lib/platform/MSWindowsScreen.h @@ -138,6 +138,7 @@ class MSWindowsScreen : public PlatformScreen virtual const String &getDropTarget() const; String getSecureInputApp() const override; void activateWindowAt(SInt32 x, SInt32 y) override; + void fakeTouchClick(SInt32 x, SInt32 y) override; protected: // IPlatformScreen overrides From 561cf7011645055099aaee3cf613ad351a5f35b0 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Mon, 2 Mar 2026 12:07:16 +0000 Subject: [PATCH 06/25] chore: add touch event logging --- src/lib/platform/MSWindowsDesks.cpp | 46 +++++++++++++++++++++++++--- src/lib/platform/MSWindowsHook.cpp | 8 +++++ src/lib/platform/MSWindowsScreen.cpp | 19 ++++++++++-- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index 907750939..48bb70f2d 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -444,11 +444,13 @@ LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM w if (self) { UINT32 pointerId = GET_POINTERID_WPARAM(wParam); DWORD pointerType = PT_POINTER; - GetPointerType(pointerId, &pointerType); + BOOL gotType = GetPointerType(pointerId, &pointerType); + LOG((CLOG_DEBUG "secondary WM_POINTERDOWN: pointerId=%u gotType=%d pointerType=%u", + pointerId, gotType ? 1 : 0, pointerType)); if (pointerType == PT_TOUCH || pointerType == PT_PEN) { SInt32 x = GET_X_LPARAM(lParam); SInt32 y = GET_Y_LPARAM(lParam); - LOG((CLOG_DEBUG1 "secondary WM_POINTERDOWN touch at %d,%d", x, y)); + LOG((CLOG_DEBUG "secondary WM_POINTERDOWN touch at %d,%d", x, y)); PostThreadMessage(self->m_threadID, DESKFLOW_MSG_TOUCH, static_cast(x), static_cast(y)); return 0; @@ -459,8 +461,11 @@ LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM w case WM_MOUSEMOVE: { LPARAM extraInfo = GetMessageExtraInfo(); - if ((extraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE) + if ((extraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE) { + LOG((CLOG_DEBUG "secondary WM_MOUSEMOVE: touch signature detected (extraInfo=0x%08x), keeping hider", + (DWORD)extraInfo)); break; + } MSWindowsDesks *self = reinterpret_cast( GetWindowLongPtr(hwnd, GWLP_USERDATA)); @@ -878,6 +883,28 @@ void MSWindowsDesks::registerTouchRawInput(HWND window, bool enable) if (!RegisterRawInputDevices(rids, 4, sizeof(RAWINPUTDEVICE))) { RegisterRawInputDevices(rids, 1, sizeof(RAWINPUTDEVICE)); } + + if (enable) { + UINT numDevices = 0; + if (GetRawInputDeviceList(NULL, &numDevices, sizeof(RAWINPUTDEVICELIST)) == 0 && numDevices > 0) { + RAWINPUTDEVICELIST *devices = new RAWINPUTDEVICELIST[numDevices]; + if (GetRawInputDeviceList(devices, &numDevices, sizeof(RAWINPUTDEVICELIST)) != (UINT)-1) { + LOG((CLOG_DEBUG "raw input: %u device(s) connected", numDevices)); + for (UINT i = 0; i < numDevices; ++i) { + if (devices[i].dwType == RIM_TYPEHID) { + RID_DEVICE_INFO info = {}; + UINT infoSize = sizeof(info); + info.cbSize = sizeof(info); + GetRawInputDeviceInfo(devices[i].hDevice, RIDI_DEVICEINFO, &info, &infoSize); + LOG((CLOG_DEBUG " HID: VID=0x%04x PID=0x%04x page=0x%02x usage=0x%02x", + info.hid.dwVendorId, info.hid.dwProductId, + info.hid.usUsagePage, info.hid.usUsage)); + } + } + } + delete[] devices; + } + } } void MSWindowsDesks::deskThread(void *vdesk) @@ -929,6 +956,14 @@ void MSWindowsDesks::deskThread(void *vdesk) reinterpret_cast(msg.lParam), RID_INPUT, buffer, &size, sizeof(RAWINPUTHEADER)) != static_cast(-1)) { RAWINPUT *raw = reinterpret_cast(buffer); + + LOG((CLOG_DEBUG "WM_INPUT: type=%s isPrimary=%d count=%u sizeHid=%u", + raw->header.dwType == RIM_TYPEHID ? "HID" + : raw->header.dwType == RIM_TYPEMOUSE ? "mouse" : "other", + m_isPrimary ? 1 : 0, + raw->header.dwType == RIM_TYPEHID ? raw->data.hid.dwCount : 0, + raw->header.dwType == RIM_TYPEHID ? raw->data.hid.dwSizeHid : 0)); + if (raw->header.dwType == RIM_TYPEHID && raw->data.hid.dwCount > 0 && raw->data.hid.dwSizeHid > 0) { auto it = m_hidTouchDevices.find(raw->header.hDevice); @@ -939,9 +974,12 @@ void MSWindowsDesks::deskThread(void *vdesk) SInt32 tx, ty; if (it->second.valid && parseHidTouch(raw, it->second, tx, ty)) { - LOG((CLOG_DEBUG1 "desk raw touch at %d,%d", tx, ty)); + LOG((CLOG_DEBUG "WM_INPUT: parsed HID touch at %d,%d, posting DESKFLOW_MSG_TOUCH", tx, ty)); PostThreadMessage(m_threadID, DESKFLOW_MSG_TOUCH, static_cast(tx), static_cast(ty)); + } else { + LOG((CLOG_DEBUG "WM_INPUT: HID data present but parseHidTouch returned false (valid=%d)", + it->second.valid ? 1 : 0)); } } } diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index fa85e41d4..e80437835 100644 --- a/src/lib/platform/MSWindowsHook.cpp +++ b/src/lib/platform/MSWindowsHook.cpp @@ -595,11 +595,19 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) // touch-synthesized mouse events as injected (LLMHF_INJECTED). if (g_touchActivateScreen && g_mode == kHOOK_RELAY_EVENTS) { bool isTouchEvent = (info->dwExtraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE; + + if (wParam == WM_LBUTTONDOWN) { + LOG((CLOG_DEBUG "hook: WM_LBUTTONDOWN extraInfo=0x%08x touchSig=%s isPrimary=%d mode=%d", + (DWORD)info->dwExtraInfo, isTouchEvent ? "yes" : "no", g_isPrimary, g_mode)); + } + if (isTouchEvent && (wParam == WM_LBUTTONDOWN || wParam == WM_MOUSEMOVE)) { SInt32 x = static_cast(info->pt.x); SInt32 y = static_cast(info->pt.y); + LOG((CLOG_DEBUG "hook: touch at %d,%d posting DESKFLOW_MSG_TOUCH", x, y)); PostThreadMessage(g_threadID, DESKFLOW_MSG_TOUCH, x, y); if (g_isPrimary) { + LOG((CLOG_DEBUG "hook: eating touch event (relay mode)")); return 1; } } diff --git a/src/lib/platform/MSWindowsScreen.cpp b/src/lib/platform/MSWindowsScreen.cpp index d2d60599c..79e45149c 100644 --- a/src/lib/platform/MSWindowsScreen.cpp +++ b/src/lib/platform/MSWindowsScreen.cpp @@ -973,11 +973,17 @@ bool MSWindowsScreen::onPreDispatch(HWND hwnd, UINT message, WPARAM wParam, LPAR return true; case DESKFLOW_MSG_TOUCH: + LOG((CLOG_DEBUG "DESKFLOW_MSG_TOUCH: touchActive=%d isOnScreen=%d isPrimary=%d at %d,%d", + m_touchActivateScreen ? 1 : 0, m_isOnScreen ? 1 : 0, m_isPrimary ? 1 : 0, + (int)wParam, (int)lParam)); if (!m_touchActivateScreen || m_isOnScreen) return true; { - if (m_touchDebounceTimer.getTime() < kTouchDebounceTime) + if (m_touchDebounceTimer.getTime() < kTouchDebounceTime) { + LOG((CLOG_DEBUG "DESKFLOW_MSG_TOUCH: debounced (%.0fms elapsed)", + m_touchDebounceTimer.getTime() * 1000.0)); return true; + } m_touchDebounceTimer.reset(); SInt32 x = static_cast(wParam); @@ -1466,14 +1472,21 @@ bool MSWindowsScreen::onPointerInput(WPARAM wParam, LPARAM lParam) { UINT32 pointerId = GET_POINTERID_WPARAM(wParam); - if (!isPointerTypeTouch(pointerId)) + if (!isPointerTypeTouch(pointerId)) { + DWORD pointerType = PT_POINTER; + GetPointerType(pointerId, &pointerType); + LOG((CLOG_DEBUG "WM_POINTER: non-touch type=%u (1=generic,2=touch,3=pen,4=mouse)", pointerType)); return false; + } if (!m_touchActivateScreen || m_isOnScreen) return false; - if (m_touchDebounceTimer.getTime() < kTouchDebounceTime) + if (m_touchDebounceTimer.getTime() < kTouchDebounceTime) { + LOG((CLOG_DEBUG "WM_POINTER: touch debounced (%.0fms elapsed, %.0fms required)", + m_touchDebounceTimer.getTime() * 1000.0, kTouchDebounceTime * 1000.0)); return true; + } m_touchDebounceTimer.reset(); POINT pt; From aff75ee75962009b445415a8cda5fefc4c8e35e3 Mon Sep 17 00:00:00 2001 From: Stefan Verleysen Date: Mon, 2 Mar 2026 20:40:11 -0500 Subject: [PATCH 07/25] fix: client touch click on fullscreen apps after screen switch Co-Authored-By: Claude Opus 4.6 --- src/lib/client/Client.cpp | 12 -- src/lib/client/Client.h | 3 - src/lib/platform/MSWindowsDesks.cpp | 239 +++++++++++++++++++++------- src/lib/platform/MSWindowsDesks.h | 6 + src/lib/server/PrimaryClient.cpp | 5 + src/lib/server/PrimaryClient.h | 1 + src/lib/server/Server.cpp | 3 +- 7 files changed, 195 insertions(+), 74 deletions(-) diff --git a/src/lib/client/Client.cpp b/src/lib/client/Client.cpp index a295a239a..d06f6ae0d 100644 --- a/src/lib/client/Client.cpp +++ b/src/lib/client/Client.cpp @@ -246,15 +246,6 @@ void Client::enter(SInt32 xAbs, SInt32 yAbs, UInt32, KeyModifierMask mask, bool) m_screen->mouseMove(xAbs, yAbs); m_screen->enter(mask); - if (m_pendingTouchActivation) { - m_pendingTouchActivation = false; - ARCH->sleep(0.1); - LOG((CLOG_DEBUG1 "touch: replaying click at %d,%d (cursor at %d,%d)", - m_touchActivateX, m_touchActivateY, xAbs, yAbs)); - m_screen->activateWindowAt(m_touchActivateX, m_touchActivateY); - m_screen->fakeTouchClick(m_touchActivateX, m_touchActivateY); - } - if (m_sendFileThread) { StreamChunker::interruptFile(); m_sendFileThread.reset(nullptr); @@ -739,9 +730,6 @@ void Client::handleGrabInput(const Event &event, void *) IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); if (m_server != NULL) { LOG((CLOG_DEBUG1 "requesting grab input at %d,%d", info->m_x, info->m_y)); - m_pendingTouchActivation = true; - m_touchActivateX = info->m_x; - m_touchActivateY = info->m_y; m_server->grabInput(info->m_x, info->m_y); } } diff --git a/src/lib/client/Client.h b/src/lib/client/Client.h index 217d744a1..6389c015e 100644 --- a/src/lib/client/Client.h +++ b/src/lib/client/Client.h @@ -260,7 +260,4 @@ class Client : public IClient, public INode size_t m_maximumClipboardSize; deskflow::ClientArgs m_args; size_t m_resolvedAddressesCount = 0; - bool m_pendingTouchActivation = false; - SInt32 m_touchActivateX = 0; - SInt32 m_touchActivateY = 0; }; diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index 48bb70f2d..85d316d46 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -96,6 +96,8 @@ typedef LONG NTSTATUS; // x; y #define DESKFLOW_MSG_FAKE_TOUCH DESKFLOW_HOOK_LAST_MSG + 13 +#define TOUCH_CLICK_TIMER_ID 1 + static void send_keyboard_input(WORD wVk, WORD wScan, DWORD dwFlags) { INPUT inp; @@ -451,6 +453,9 @@ LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM w SInt32 x = GET_X_LPARAM(lParam); SInt32 y = GET_Y_LPARAM(lParam); LOG((CLOG_DEBUG "secondary WM_POINTERDOWN touch at %d,%d", x, y)); + self->m_pendingTouchUp = true; + self->m_pendingTouchX = x; + self->m_pendingTouchY = y; PostThreadMessage(self->m_threadID, DESKFLOW_MSG_TOUCH, static_cast(x), static_cast(y)); return 0; @@ -459,6 +464,47 @@ LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM w break; } + case WM_POINTERUP: { + MSWindowsDesks *self = reinterpret_cast( + GetWindowLongPtr(hwnd, GWLP_USERDATA)); + if (self && self->m_pendingTouchUp) { + SInt32 x = self->m_pendingTouchX; + SInt32 y = self->m_pendingTouchY; + if (self->m_isOnScreen) { + self->m_pendingTouchUp = false; + SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + ULONGLONG elapsed = GetTickCount64() - self->m_touchHideTime; + LOG((CLOG_DEBUG "touch: finger up, hider hidden, %llu ms since shrink, injecting at %d,%d", + elapsed, x, y)); + PostThreadMessage(GetCurrentThreadId(), DESKFLOW_MSG_FAKE_TOUCH, + static_cast(x), static_cast(y)); + } else { + self->m_touchLifted = true; + LOG((CLOG_DEBUG "touch: finger up before enter, will fire on deskEnter at %d,%d", x, y)); + } + } + return 0; + } + + case WM_TIMER: { + if (wParam == TOUCH_CLICK_TIMER_ID) { + KillTimer(hwnd, TOUCH_CLICK_TIMER_ID); + MSWindowsDesks *self = reinterpret_cast( + GetWindowLongPtr(hwnd, GWLP_USERDATA)); + if (self) { + ULONGLONG elapsed = GetTickCount64() - self->m_touchHideTime; + LOG((CLOG_DEBUG "touch: timer fired, %llu ms after hide, injecting at %d,%d", + elapsed, self->m_pendingTouchX, self->m_pendingTouchY)); + PostThreadMessage(GetCurrentThreadId(), DESKFLOW_MSG_FAKE_TOUCH, + static_cast(self->m_pendingTouchX), + static_cast(self->m_pendingTouchY)); + } + return 0; + } + break; + } + case WM_MOUSEMOVE: { LPARAM extraInfo = GetMessageExtraInfo(); if ((extraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE) { @@ -502,61 +548,112 @@ void MSWindowsDesks::deskMouseMove(SInt32 x, SInt32 y) const void MSWindowsDesks::deskFakeTouchClick(SInt32 x, SInt32 y) const { - static bool s_touchReady = false; - - if (!s_touchReady) { - if (!InitializeTouchInjection(1, TOUCH_FEEDBACK_DEFAULT)) { - DWORD err = GetLastError(); - LOG((CLOG_WARN "touch: InitializeTouchInjection failed, err=%d", err)); - deskMouseMove(x, y); - send_mouse_input(MOUSEEVENTF_LEFTDOWN, 0, 0, 0); - send_mouse_input(MOUSEEVENTF_LEFTUP, 0, 0, 0); - return; - } - LOG((CLOG_DEBUG1 "touch: InitializeTouchInjection succeeded")); - s_touchReady = true; + ULONGLONG elapsed = GetTickCount64() - m_touchHideTime; + + POINT pt = {x, y}; + HWND target = WindowFromPoint(pt); + HWND fg = GetForegroundWindow(); + + char targetClass[128] = {0}; + char targetTitle[128] = {0}; + char fgClass[128] = {0}; + char fgTitle[128] = {0}; + if (target) { + GetClassNameA(target, targetClass, sizeof(targetClass)); + GetWindowTextA(target, targetTitle, sizeof(targetTitle)); + } + if (fg) { + GetClassNameA(fg, fgClass, sizeof(fgClass)); + GetWindowTextA(fg, fgTitle, sizeof(fgTitle)); } - POINTER_TOUCH_INFO contact; - memset(&contact, 0, sizeof(POINTER_TOUCH_INFO)); + LOG((CLOG_DEBUG "touch: injecting at %d,%d %llu ms after hide target=\"%s\" [%s] fg=\"%s\" [%s]", + x, y, elapsed, targetTitle, targetClass, fgTitle, fgClass)); - contact.pointerInfo.pointerType = PT_TOUCH; - contact.pointerInfo.pointerId = 0; - contact.pointerInfo.ptPixelLocation.x = x; - contact.pointerInfo.ptPixelLocation.y = y; + if (target) { + HWND root = GetAncestor(target, GA_ROOT); + if (root && root != fg) { + DWORD targetThread = GetWindowThreadProcessId(root, NULL); + DWORD curThread = GetCurrentThreadId(); - contact.touchFlags = TOUCH_FLAG_NONE; - contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE; - contact.orientation = 90; - contact.pressure = 32000; - contact.rcContact.left = x - 2; - contact.rcContact.right = x + 2; - contact.rcContact.top = y - 2; - contact.rcContact.bottom = y + 2; + BOOL attached = FALSE; + if (targetThread != 0 && targetThread != curThread) { + attached = AttachThreadInput(targetThread, curThread, TRUE); + } - contact.pointerInfo.pointerFlags = - POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT; + // bypass foreground lock + mouse_event(MOUSEEVENTF_MOVE, 0, 0, 0, 0); - LOG((CLOG_DEBUG1 "touch: injecting DOWN at %d,%d", x, y)); + BOOL fgOk = SetForegroundWindow(root); + BringWindowToTop(root); - bool injected = false; - if (InjectTouchInput(1, &contact)) { - Sleep(30); - contact.pointerInfo.pointerFlags = POINTER_FLAG_UP; - InjectTouchInput(1, &contact); - injected = true; - LOG((CLOG_DEBUG1 "touch: injected touch at %d,%d", x, y)); - } else { - DWORD err = GetLastError(); - LOG((CLOG_WARN "touch: InjectTouchInput failed at %d,%d, err=%d", x, y, err)); + char rootTitle[128] = {0}; + GetWindowTextA(root, rootTitle, sizeof(rootTitle)); + + int waited = 0; + while (GetForegroundWindow() != root && waited < 200) { + Sleep(10); + waited += 10; + } + HWND newFg = GetForegroundWindow(); + + if (attached) { + AttachThreadInput(targetThread, curThread, FALSE); + } + + LOG((CLOG_DEBUG "touch: SetForegroundWindow(\"%s\")=%d, fg=0x%p (root=0x%p) waited=%dms", + rootTitle, fgOk, newFg, root, waited)); + } + } + + static bool touchInitialized = false; + if (!touchInitialized) { + touchInitialized = InitializeTouchInjection(1, TOUCH_FEEDBACK_NONE) != 0; + LOG((CLOG_DEBUG "touch: InitializeTouchInjection %s (err=%d)", + touchInitialized ? "ok" : "FAILED", GetLastError())); } - Sleep(50); + if (touchInitialized) { + POINTER_TOUCH_INFO contact = {}; + contact.pointerInfo.pointerType = PT_TOUCH; + contact.pointerInfo.pointerId = 0; + contact.pointerInfo.ptPixelLocation.x = x; + contact.pointerInfo.ptPixelLocation.y = y; + contact.pointerInfo.pointerFlags = + POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT; + contact.touchFlags = TOUCH_FLAG_NONE; + contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | + TOUCH_MASK_PRESSURE; + contact.orientation = 90; + contact.pressure = 32000; + contact.rcContact.left = x - 2; + contact.rcContact.right = x + 2; + contact.rcContact.top = y - 2; + contact.rcContact.bottom = y + 2; + + BOOL downOk = InjectTouchInput(1, &contact); + DWORD downErr = GetLastError(); + + Sleep(20); + + contact.pointerInfo.pointerFlags = + POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT; + BOOL updOk = InjectTouchInput(1, &contact); + + Sleep(20); + + contact.pointerInfo.pointerFlags = POINTER_FLAG_UP; + BOOL upOk = InjectTouchInput(1, &contact); + DWORD upErr = GetLastError(); + + LOG((CLOG_DEBUG "touch: InjectTouchInput down=%d(err=%d) upd=%d up=%d(err=%d) at %d,%d", + downOk, downErr, updOk, upOk, upErr, x, y)); + } deskMouseMove(x, y); send_mouse_input(MOUSEEVENTF_LEFTDOWN, 0, 0, 0); send_mouse_input(MOUSEEVENTF_LEFTUP, 0, 0, 0); - LOG((CLOG_DEBUG1 "touch: sent mouse click at %d,%d (touch injected=%d)", x, y, injected)); + LOG((CLOG_DEBUG "touch: SendInput mouse click at %d,%d", x, y)); } void MSWindowsDesks::deskMouseRelativeMove(SInt32 dx, SInt32 dy) const @@ -634,13 +731,32 @@ void MSWindowsDesks::deskEnter(Desk *desk) ReleaseCapture(); LONG_PTR exStyle = GetWindowLongPtr(desk->m_window, GWL_EXSTYLE); - // let hit-testing pass through to windows underneath SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle | WS_EX_TRANSPARENT); } else { registerTouchRawInput(desk->m_window, false); } - SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + bool touchEnter = false; + if (m_pendingTouchUp && m_touchLifted) { + touchEnter = true; + m_pendingTouchUp = false; + m_touchLifted = false; + SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + m_touchHideTime = GetTickCount64(); + SetTimer(desk->m_window, TOUCH_CLICK_TIMER_ID, 150, NULL); + LOG((CLOG_DEBUG "touch: deskEnter path, hider hidden, timer started at %d,%d", + m_pendingTouchX, m_pendingTouchY)); + } else if (m_pendingTouchUp) { + touchEnter = true; + SetWindowPos(desk->m_window, HWND_BOTTOM, m_xCenter, m_yCenter, 1, 1, + SWP_NOACTIVATE); + m_touchHideTime = GetTickCount64(); + LOG((CLOG_DEBUG "touch: deskEnter, finger still down, hider shrunk to 1x1")); + } else { + SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + } setCursorVisibility(true); @@ -648,17 +764,21 @@ void MSWindowsDesks::deskEnter(Desk *desk) SetClassLongPtr(desk->m_window, GCLP_HCURSOR, reinterpret_cast(arrow)); SetCursor(arrow); - // restore the foreground window - // XXX -- this raises the window to the top of the Z-order. we - // want it to stay wherever it was to properly support X-mouse - // (mouse over activation) but i've no idea how to do that. - // the obvious workaround of using SetWindowPos() to move it back - // after being raised doesn't work. - DWORD thisThread = GetWindowThreadProcessId(desk->m_window, NULL); - DWORD thatThread = GetWindowThreadProcessId(desk->m_foregroundWindow, NULL); - AttachThreadInput(thatThread, thisThread, TRUE); - SetForegroundWindow(desk->m_foregroundWindow); - AttachThreadInput(thatThread, thisThread, FALSE); + if (touchEnter) { + LOG((CLOG_DEBUG "touch: skipping foreground restore to preserve fullscreen app focus")); + } else { + // restore the foreground window + // XXX -- this raises the window to the top of the Z-order. we + // want it to stay wherever it was to properly support X-mouse + // (mouse over activation) but i've no idea how to do that. + // the obvious workaround of using SetWindowPos() to move it back + // after being raised doesn't work. + DWORD thisThread = GetWindowThreadProcessId(desk->m_window, NULL); + DWORD thatThread = GetWindowThreadProcessId(desk->m_foregroundWindow, NULL); + AttachThreadInput(thatThread, thisThread, TRUE); + SetForegroundWindow(desk->m_foregroundWindow); + AttachThreadInput(thatThread, thisThread, FALSE); + } EnableWindow(desk->m_window, desk->m_lowLevel ? FALSE : TRUE); desk->m_foregroundWindow = NULL; } @@ -864,8 +984,13 @@ bool MSWindowsDesks::parseHidTouch(const RAWINPUT *raw, const HidTouchDevice &de return false; } - outX = m_x + static_cast(rawX * m_w / dev.logicalMaxX); - outY = m_y + static_cast(rawY * m_h / dev.logicalMaxY); + // Touch digitizer maps to the primary monitor, not the virtual desktop + SInt32 pw = GetSystemMetrics(SM_CXSCREEN); + SInt32 ph = GetSystemMetrics(SM_CYSCREEN); + outX = static_cast(rawX * pw / dev.logicalMaxX); + outY = static_cast(rawY * ph / dev.logicalMaxY); + LOG((CLOG_DEBUG1 "touch HID: raw=%lu,%lu logMax=%lu,%lu primary=%dx%d -> %d,%d", + rawX, rawY, dev.logicalMaxX, dev.logicalMaxY, pw, ph, outX, outY)); return true; } diff --git a/src/lib/platform/MSWindowsDesks.h b/src/lib/platform/MSWindowsDesks.h index e8a868583..d71cf9b4c 100644 --- a/src/lib/platform/MSWindowsDesks.h +++ b/src/lib/platform/MSWindowsDesks.h @@ -316,4 +316,10 @@ class MSWindowsDesks bool m_stopOnDeskSwitch; std::unordered_map m_hidTouchDevices; + + bool m_pendingTouchUp = false; + bool m_touchLifted = false; + SInt32 m_pendingTouchX = 0; + SInt32 m_pendingTouchY = 0; + ULONGLONG m_touchHideTime = 0; }; diff --git a/src/lib/server/PrimaryClient.cpp b/src/lib/server/PrimaryClient.cpp index 070d4cbbf..8588ef4eb 100644 --- a/src/lib/server/PrimaryClient.cpp +++ b/src/lib/server/PrimaryClient.cpp @@ -76,6 +76,11 @@ void PrimaryClient::activateWindowAt(SInt32 x, SInt32 y) m_screen->activateWindowAt(x, y); } +void PrimaryClient::fakeTouchClick(SInt32 x, SInt32 y) +{ + m_screen->fakeTouchClick(x, y); +} + SInt32 PrimaryClient::getJumpZoneSize() const { return m_screen->getJumpZoneSize(); diff --git a/src/lib/server/PrimaryClient.h b/src/lib/server/PrimaryClient.h index 8203eab15..7f7b88bcc 100644 --- a/src/lib/server/PrimaryClient.h +++ b/src/lib/server/PrimaryClient.h @@ -84,6 +84,7 @@ class PrimaryClient : public BaseClientProxy void fakeInputEnd(); void activateWindowAt(SInt32 x, SInt32 y); + void fakeTouchClick(SInt32 x, SInt32 y); //@} //! @name accessors diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index 057054d2c..cd0fc1874 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -1371,9 +1371,8 @@ void Server::handleTouchActivatedPrimaryEvent(const Event &event, void *) switchScreen(m_primaryClient, x, y, false); - // The hook eats the original touch event, so the window under the - // touch point never receives it. Explicitly activate that window. m_primaryClient->activateWindowAt(x, y); + m_primaryClient->fakeTouchClick(x, y); m_touchSwitchCooldown.reset(); LOG((CLOG_DEBUG1 "touch switch cooldown started")); From a79838e77e47c60cc018b392f4a1fab1fa3ac8e9 Mon Sep 17 00:00:00 2001 From: Stefan Verleysen Date: Tue, 3 Mar 2026 14:31:28 -0500 Subject: [PATCH 08/25] fix: replace timing hacks with state-driven client touch injection - Async touch injection via PostThreadMessage chain (no Sleep calls) - WS_EX_LAYERED hider (alpha=1) prevents DWM fullscreen occlusion - Local cursor restored on real mouse movement (position check, not timing) - Fix InjectTouchInput pointer ID (pointerId=1, InitializeTouchInjection(2)) - WM_INPUT touch detection on secondary with m_isOnScreen guard - Foreground window restored before injection - Bidirectional touch switch cooldown - Remove m_deskLeaveTime, m_touchHideTime, WM_TIMER, polling loops Co-Authored-By: Claude Opus 4.6 --- src/lib/platform/MSWindowsDesks.cpp | 236 ++++++++++++++-------------- src/lib/platform/MSWindowsDesks.h | 5 +- src/lib/server/Server.cpp | 11 ++ 3 files changed, 127 insertions(+), 125 deletions(-) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index 85d316d46..fd5ef9297 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -95,8 +95,8 @@ typedef LONG NTSTATUS; #define DESKFLOW_MSG_FAKE_INPUT DESKFLOW_HOOK_LAST_MSG + 12 // x; y #define DESKFLOW_MSG_FAKE_TOUCH DESKFLOW_HOOK_LAST_MSG + 13 - -#define TOUCH_CLICK_TIMER_ID 1 +#define DESKFLOW_MSG_TOUCH_UPDATE DESKFLOW_HOOK_LAST_MSG + 14 +#define DESKFLOW_MSG_TOUCH_UP DESKFLOW_HOOK_LAST_MSG + 15 static void send_keyboard_input(WORD wVk, WORD wScan, DWORD dwFlags) { @@ -134,7 +134,6 @@ MSWindowsDesks::MSWindowsDesks( : m_isPrimary(isPrimary), m_noHooks(noHooks), m_isOnScreen(m_isPrimary), - m_deskLeaveTime(0), m_x(0), m_y(0), m_w(0), @@ -434,6 +433,8 @@ LRESULT CALLBACK MSWindowsDesks::primaryDeskProc(HWND hwnd, UINT msg, WPARAM wPa return DefWindowProc(hwnd, msg, wParam, lParam); } +void setCursorVisibility(bool visible); + LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { @@ -474,9 +475,7 @@ LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM w self->m_pendingTouchUp = false; SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); - ULONGLONG elapsed = GetTickCount64() - self->m_touchHideTime; - LOG((CLOG_DEBUG "touch: finger up, hider hidden, %llu ms since shrink, injecting at %d,%d", - elapsed, x, y)); + LOG((CLOG_DEBUG "touch: finger up, hider hidden, injecting at %d,%d", x, y)); PostThreadMessage(GetCurrentThreadId(), DESKFLOW_MSG_FAKE_TOUCH, static_cast(x), static_cast(y)); } else { @@ -487,41 +486,26 @@ LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM w return 0; } - case WM_TIMER: { - if (wParam == TOUCH_CLICK_TIMER_ID) { - KillTimer(hwnd, TOUCH_CLICK_TIMER_ID); - MSWindowsDesks *self = reinterpret_cast( - GetWindowLongPtr(hwnd, GWLP_USERDATA)); - if (self) { - ULONGLONG elapsed = GetTickCount64() - self->m_touchHideTime; - LOG((CLOG_DEBUG "touch: timer fired, %llu ms after hide, injecting at %d,%d", - elapsed, self->m_pendingTouchX, self->m_pendingTouchY)); - PostThreadMessage(GetCurrentThreadId(), DESKFLOW_MSG_FAKE_TOUCH, - static_cast(self->m_pendingTouchX), - static_cast(self->m_pendingTouchY)); - } - return 0; - } - break; - } - case WM_MOUSEMOVE: { LPARAM extraInfo = GetMessageExtraInfo(); if ((extraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE) { - LOG((CLOG_DEBUG "secondary WM_MOUSEMOVE: touch signature detected (extraInfo=0x%08x), keeping hider", - (DWORD)extraInfo)); break; } MSWindowsDesks *self = reinterpret_cast( GetWindowLongPtr(hwnd, GWLP_USERDATA)); if (self && IsWindowVisible(hwnd)) { - if (GetTickCount64() - self->m_deskLeaveTime < 200) { + // deskLeave centers the cursor; ignore moves at that position + // so only real local input dismisses the hider + POINT pt; + GetCursorPos(&pt); + if (pt.x == self->m_xCenter && pt.y == self->m_yCenter) { break; } ReleaseCapture(); SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + setCursorVisibility(true); HCURSOR arrow = LoadCursor(NULL, IDC_ARROW); SetClassLongPtr(hwnd, GCLP_HCURSOR, reinterpret_cast(arrow)); SetCursor(arrow); @@ -548,31 +532,17 @@ void MSWindowsDesks::deskMouseMove(SInt32 x, SInt32 y) const void MSWindowsDesks::deskFakeTouchClick(SInt32 x, SInt32 y) const { - ULONGLONG elapsed = GetTickCount64() - m_touchHideTime; - POINT pt = {x, y}; HWND target = WindowFromPoint(pt); HWND fg = GetForegroundWindow(); - char targetClass[128] = {0}; - char targetTitle[128] = {0}; - char fgClass[128] = {0}; - char fgTitle[128] = {0}; - if (target) { - GetClassNameA(target, targetClass, sizeof(targetClass)); - GetWindowTextA(target, targetTitle, sizeof(targetTitle)); - } - if (fg) { - GetClassNameA(fg, fgClass, sizeof(fgClass)); - GetWindowTextA(fg, fgTitle, sizeof(fgTitle)); - } - - LOG((CLOG_DEBUG "touch: injecting at %d,%d %llu ms after hide target=\"%s\" [%s] fg=\"%s\" [%s]", - x, y, elapsed, targetTitle, targetClass, fgTitle, fgClass)); + LOG((CLOG_DEBUG "touch: injecting at %d,%d target=0x%08x fg=0x%08x", + x, y, target, fg)); if (target) { HWND root = GetAncestor(target, GA_ROOT); if (root && root != fg) { + LOG((CLOG_DEBUG "touch: target root=0x%08x != fg, activating", root)); DWORD targetThread = GetWindowThreadProcessId(root, NULL); DWORD curThread = GetCurrentThreadId(); @@ -581,42 +551,25 @@ void MSWindowsDesks::deskFakeTouchClick(SInt32 x, SInt32 y) const attached = AttachThreadInput(targetThread, curThread, TRUE); } - // bypass foreground lock mouse_event(MOUSEEVENTF_MOVE, 0, 0, 0, 0); - - BOOL fgOk = SetForegroundWindow(root); + SetForegroundWindow(root); BringWindowToTop(root); - char rootTitle[128] = {0}; - GetWindowTextA(root, rootTitle, sizeof(rootTitle)); - - int waited = 0; - while (GetForegroundWindow() != root && waited < 200) { - Sleep(10); - waited += 10; - } - HWND newFg = GetForegroundWindow(); - if (attached) { AttachThreadInput(targetThread, curThread, FALSE); } - - LOG((CLOG_DEBUG "touch: SetForegroundWindow(\"%s\")=%d, fg=0x%p (root=0x%p) waited=%dms", - rootTitle, fgOk, newFg, root, waited)); } } static bool touchInitialized = false; if (!touchInitialized) { - touchInitialized = InitializeTouchInjection(1, TOUCH_FEEDBACK_NONE) != 0; - LOG((CLOG_DEBUG "touch: InitializeTouchInjection %s (err=%d)", - touchInitialized ? "ok" : "FAILED", GetLastError())); + touchInitialized = InitializeTouchInjection(2, TOUCH_FEEDBACK_NONE) != 0; } if (touchInitialized) { POINTER_TOUCH_INFO contact = {}; contact.pointerInfo.pointerType = PT_TOUCH; - contact.pointerInfo.pointerId = 0; + contact.pointerInfo.pointerId = 1; contact.pointerInfo.ptPixelLocation.x = x; contact.pointerInfo.ptPixelLocation.y = y; contact.pointerInfo.pointerFlags = @@ -631,29 +584,23 @@ void MSWindowsDesks::deskFakeTouchClick(SInt32 x, SInt32 y) const contact.rcContact.top = y - 2; contact.rcContact.bottom = y + 2; - BOOL downOk = InjectTouchInput(1, &contact); - DWORD downErr = GetLastError(); - - Sleep(20); - - contact.pointerInfo.pointerFlags = - POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT; - BOOL updOk = InjectTouchInput(1, &contact); - - Sleep(20); - - contact.pointerInfo.pointerFlags = POINTER_FLAG_UP; - BOOL upOk = InjectTouchInput(1, &contact); - DWORD upErr = GetLastError(); + if (!InjectTouchInput(1, &contact)) { + LOG((CLOG_DEBUG "touch: InjectTouchInput DOWN failed, error=%lu, falling back to mouse", + GetLastError())); + deskMouseMove(x, y); + send_mouse_input(MOUSEEVENTF_LEFTDOWN, 0, 0, 0); + send_mouse_input(MOUSEEVENTF_LEFTUP, 0, 0, 0); + return; + } - LOG((CLOG_DEBUG "touch: InjectTouchInput down=%d(err=%d) upd=%d up=%d(err=%d) at %d,%d", - downOk, downErr, updOk, upOk, upErr, x, y)); + PostThreadMessage(GetCurrentThreadId(), DESKFLOW_MSG_TOUCH_UPDATE, + static_cast(x), static_cast(y)); + return; } deskMouseMove(x, y); send_mouse_input(MOUSEEVENTF_LEFTDOWN, 0, 0, 0); send_mouse_input(MOUSEEVENTF_LEFTUP, 0, 0, 0); - LOG((CLOG_DEBUG "touch: SendInput mouse click at %d,%d", x, y)); } void MSWindowsDesks::deskMouseRelativeMove(SInt32 dx, SInt32 dy) const @@ -727,32 +674,23 @@ void setCursorVisibility(bool visible) void MSWindowsDesks::deskEnter(Desk *desk) { + registerTouchRawInput(desk->m_window, false); + if (!m_isPrimary) { ReleaseCapture(); LONG_PTR exStyle = GetWindowLongPtr(desk->m_window, GWL_EXSTYLE); - SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle | WS_EX_TRANSPARENT); - } else { - registerTouchRawInput(desk->m_window, false); + exStyle = (exStyle | WS_EX_TRANSPARENT) & ~WS_EX_LAYERED; + SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle); } bool touchEnter = false; - if (m_pendingTouchUp && m_touchLifted) { + if (m_pendingTouchUp) { touchEnter = true; m_pendingTouchUp = false; m_touchLifted = false; SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); - m_touchHideTime = GetTickCount64(); - SetTimer(desk->m_window, TOUCH_CLICK_TIMER_ID, 150, NULL); - LOG((CLOG_DEBUG "touch: deskEnter path, hider hidden, timer started at %d,%d", - m_pendingTouchX, m_pendingTouchY)); - } else if (m_pendingTouchUp) { - touchEnter = true; - SetWindowPos(desk->m_window, HWND_BOTTOM, m_xCenter, m_yCenter, 1, 1, - SWP_NOACTIVATE); - m_touchHideTime = GetTickCount64(); - LOG((CLOG_DEBUG "touch: deskEnter, finger still down, hider shrunk to 1x1")); } else { SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); @@ -764,21 +702,27 @@ void MSWindowsDesks::deskEnter(Desk *desk) SetClassLongPtr(desk->m_window, GCLP_HCURSOR, reinterpret_cast(arrow)); SetCursor(arrow); - if (touchEnter) { - LOG((CLOG_DEBUG "touch: skipping foreground restore to preserve fullscreen app focus")); - } else { - // restore the foreground window - // XXX -- this raises the window to the top of the Z-order. we - // want it to stay wherever it was to properly support X-mouse - // (mouse over activation) but i've no idea how to do that. - // the obvious workaround of using SetWindowPos() to move it back - // after being raised doesn't work. + // restore the foreground window + // XXX -- this raises the window to the top of the Z-order. we + // want it to stay wherever it was to properly support X-mouse + // (mouse over activation) but i've no idea how to do that. + // the obvious workaround of using SetWindowPos() to move it back + // after being raised doesn't work. + if (desk->m_foregroundWindow) { DWORD thisThread = GetWindowThreadProcessId(desk->m_window, NULL); DWORD thatThread = GetWindowThreadProcessId(desk->m_foregroundWindow, NULL); AttachThreadInput(thatThread, thisThread, TRUE); SetForegroundWindow(desk->m_foregroundWindow); AttachThreadInput(thatThread, thisThread, FALSE); } + + if (touchEnter) { + LOG((CLOG_DEBUG "touch: deskEnter, injecting at %d,%d", + m_pendingTouchX, m_pendingTouchY)); + PostThreadMessage(desk->m_threadID, DESKFLOW_MSG_FAKE_TOUCH, + static_cast(m_pendingTouchX), + static_cast(m_pendingTouchY)); + } EnableWindow(desk->m_window, desk->m_lowLevel ? FALSE : TRUE); desk->m_foregroundWindow = NULL; } @@ -856,16 +800,18 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) SetClassLongPtr(desk->m_window, GCLP_HCURSOR, reinterpret_cast(m_cursor)); LONG_PTR exStyle = GetWindowLongPtr(desk->m_window, GWL_EXSTYLE); - SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle & ~WS_EX_TRANSPARENT); + exStyle = (exStyle & ~WS_EX_TRANSPARENT) | WS_EX_LAYERED; + SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle); SetWindowPos(desk->m_window, HWND_TOPMOST, m_x, m_y, m_w, m_h, SWP_NOACTIVATE | SWP_SHOWWINDOW); + SetLayeredWindowAttributes(desk->m_window, 0, 1, LWA_ALPHA); SetCapture(desk->m_window); - // brief delay for the hider window's blank cursor to take effect ARCH->sleep(0.03); - m_deskLeaveTime = GetTickCount64(); deskMouseMove(m_xCenter, m_yCenter); + + registerTouchRawInput(desk->m_window, true); } } @@ -996,18 +942,13 @@ bool MSWindowsDesks::parseHidTouch(const RAWINPUT *raw, const HidTouchDevice &de void MSWindowsDesks::registerTouchRawInput(HWND window, bool enable) { - RAWINPUTDEVICE rids[4] = {}; - DWORD flags = enable ? RIDEV_INPUTSINK : RIDEV_REMOVE; - HWND target = enable ? window : NULL; - - rids[0] = {0x0D, 0x04, flags, target}; - rids[1] = {0x0D, 0x05, flags, target}; - rids[2] = {0x0D, 0x01, flags, target}; - rids[3] = {0x0D, 0x02, flags, target}; + RAWINPUTDEVICE rid = {}; + rid.usUsagePage = 0x0D; + rid.usUsage = 0x04; + rid.dwFlags = enable ? RIDEV_INPUTSINK : RIDEV_REMOVE; + rid.hwndTarget = enable ? window : NULL; - if (!RegisterRawInputDevices(rids, 4, sizeof(RAWINPUTDEVICE))) { - RegisterRawInputDevices(rids, 1, sizeof(RAWINPUTDEVICE)); - } + RegisterRawInputDevices(&rid, 1, sizeof(RAWINPUTDEVICE)); if (enable) { UINT numDevices = 0; @@ -1100,11 +1041,17 @@ void MSWindowsDesks::deskThread(void *vdesk) SInt32 tx, ty; if (it->second.valid && parseHidTouch(raw, it->second, tx, ty)) { LOG((CLOG_DEBUG "WM_INPUT: parsed HID touch at %d,%d, posting DESKFLOW_MSG_TOUCH", tx, ty)); + if (!m_isPrimary && !m_isOnScreen) { + m_pendingTouchUp = true; + m_pendingTouchX = tx; + m_pendingTouchY = ty; + } PostThreadMessage(m_threadID, DESKFLOW_MSG_TOUCH, static_cast(tx), static_cast(ty)); - } else { - LOG((CLOG_DEBUG "WM_INPUT: HID data present but parseHidTouch returned false (valid=%d)", - it->second.valid ? 1 : 0)); + } else if (!m_isPrimary && m_pendingTouchUp && it->second.valid) { + // tip switch went off on secondary; finger lifted + m_touchLifted = true; + LOG((CLOG_DEBUG "WM_INPUT: HID tip off, finger lifted")); } } } @@ -1175,6 +1122,53 @@ void MSWindowsDesks::deskThread(void *vdesk) deskFakeTouchClick(static_cast(msg.wParam), static_cast(msg.lParam)); break; + case DESKFLOW_MSG_TOUCH_UPDATE: { + POINTER_TOUCH_INFO contact = {}; + contact.pointerInfo.pointerType = PT_TOUCH; + contact.pointerInfo.pointerId = 1; + contact.pointerInfo.ptPixelLocation.x = static_cast(msg.wParam); + contact.pointerInfo.ptPixelLocation.y = static_cast(msg.lParam); + contact.pointerInfo.pointerFlags = + POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT; + contact.touchFlags = TOUCH_FLAG_NONE; + contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | + TOUCH_MASK_PRESSURE; + contact.orientation = 90; + contact.pressure = 32000; + contact.rcContact.left = contact.pointerInfo.ptPixelLocation.x - 2; + contact.rcContact.right = contact.pointerInfo.ptPixelLocation.x + 2; + contact.rcContact.top = contact.pointerInfo.ptPixelLocation.y - 2; + contact.rcContact.bottom = contact.pointerInfo.ptPixelLocation.y + 2; + InjectTouchInput(1, &contact); + PostThreadMessage(GetCurrentThreadId(), DESKFLOW_MSG_TOUCH_UP, + msg.wParam, msg.lParam); + break; + } + + case DESKFLOW_MSG_TOUCH_UP: { + POINTER_TOUCH_INFO contact = {}; + contact.pointerInfo.pointerType = PT_TOUCH; + contact.pointerInfo.pointerId = 1; + contact.pointerInfo.ptPixelLocation.x = static_cast(msg.wParam); + contact.pointerInfo.ptPixelLocation.y = static_cast(msg.lParam); + contact.pointerInfo.pointerFlags = POINTER_FLAG_UP; + contact.touchFlags = TOUCH_FLAG_NONE; + contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | + TOUCH_MASK_PRESSURE; + contact.orientation = 90; + contact.pressure = 32000; + contact.rcContact.left = contact.pointerInfo.ptPixelLocation.x - 2; + contact.rcContact.right = contact.pointerInfo.ptPixelLocation.x + 2; + contact.rcContact.top = contact.pointerInfo.ptPixelLocation.y - 2; + contact.rcContact.bottom = contact.pointerInfo.ptPixelLocation.y + 2; + InjectTouchInput(1, &contact); + + deskMouseMove(static_cast(msg.wParam), static_cast(msg.lParam)); + send_mouse_input(MOUSEEVENTF_LEFTDOWN, 0, 0, 0); + send_mouse_input(MOUSEEVENTF_LEFTUP, 0, 0, 0); + break; + } + case DESKFLOW_MSG_FAKE_WHEEL: // XXX -- add support for x-axis scrolling if (msg.lParam != 0) { diff --git a/src/lib/platform/MSWindowsDesks.h b/src/lib/platform/MSWindowsDesks.h index d71cf9b4c..b042c0f21 100644 --- a/src/lib/platform/MSWindowsDesks.h +++ b/src/lib/platform/MSWindowsDesks.h @@ -271,9 +271,6 @@ class MSWindowsDesks // true if mouse has entered the screen bool m_isOnScreen; - // suppress WM_MOUSEMOVE hider dismissal briefly after deskLeave - ULONGLONG m_deskLeaveTime; - // our resources ATOM m_deskClass; HCURSOR m_cursor; @@ -321,5 +318,5 @@ class MSWindowsDesks bool m_touchLifted = false; SInt32 m_pendingTouchX = 0; SInt32 m_pendingTouchY = 0; - ULONGLONG m_touchHideTime = 0; + }; diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index cd0fc1874..e52ff4e68 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -485,6 +485,10 @@ void Server::switchScreen(BaseClientProxy *dst, SInt32 x, SInt32 y, bool forScre } #endif + if (m_active == m_primaryClient) { + m_touchSwitchCooldown.reset(); + } + // cut over m_active = dst; @@ -1355,6 +1359,13 @@ void Server::handleTouchActivatedPrimaryEvent(const Event &event, void *) IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); LOG((CLOG_DEBUG1 "touch activated primary at %d,%d", info->m_x, info->m_y)); + double elapsed = m_touchSwitchCooldown.getTime(); + if (elapsed > 0.0 && elapsed < kTouchSwitchCooldownTime) { + LOG((CLOG_DEBUG1 "touch activation blocked by switch cooldown (%.2fs remaining)", + kTouchSwitchCooldownTime - elapsed)); + return; + } + if (m_active != m_primaryClient) { m_active->setJumpCursorPos(m_x, m_y); From 18ff1c236c01646419732ac28b78d94b98bab088 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 12:31:39 +0000 Subject: [PATCH 09/25] fix: manage cursor visibility during touch injection to emulate Windows behavior --- src/lib/platform/MSWindowsDesks.cpp | 30 +++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index fd5ef9297..baac60aab 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -98,6 +98,10 @@ typedef LONG NTSTATUS; #define DESKFLOW_MSG_TOUCH_UPDATE DESKFLOW_HOOK_LAST_MSG + 14 #define DESKFLOW_MSG_TOUCH_UP DESKFLOW_HOOK_LAST_MSG + 15 +// true when cursor is hidden due to touch injection — show on next mouse move. +// accessed only from the desk thread, so no synchronization needed. +static bool s_touchCursorHidden = false; + static void send_keyboard_input(WORD wVk, WORD wScan, DWORD dwFlags) { INPUT inp; @@ -674,6 +678,7 @@ void setCursorVisibility(bool visible) void MSWindowsDesks::deskEnter(Desk *desk) { + s_touchCursorHidden = false; registerTouchRawInput(desk->m_window, false); if (!m_isPrimary) { @@ -685,17 +690,21 @@ void MSWindowsDesks::deskEnter(Desk *desk) } bool touchEnter = false; - if (m_pendingTouchUp) { + if (m_pendingTouchUp && m_touchLifted) { + // Finger already lifted — safe to inject now, no real touch conflict. touchEnter = true; m_pendingTouchUp = false; m_touchLifted = false; - SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); - } else { - SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + } else if (m_pendingTouchUp) { + // Finger still down — defer injection to WM_POINTERUP / HID lift. + // Leave m_pendingTouchUp set so secondaryDeskProc WM_POINTERUP + // (or HID tip-off) triggers injection after the real touch ends. + LOG((CLOG_DEBUG "touch: deskEnter, finger still down, deferring injection")); } + SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + setCursorVisibility(true); HCURSOR arrow = LoadCursor(NULL, IDC_ARROW); @@ -729,6 +738,7 @@ void MSWindowsDesks::deskEnter(Desk *desk) void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) { + s_touchCursorHidden = false; setCursorVisibility(false); if (m_isPrimary) { @@ -1111,6 +1121,10 @@ void MSWindowsDesks::deskThread(void *vdesk) break; case DESKFLOW_MSG_FAKE_MOVE: + if (s_touchCursorHidden) { + setCursorVisibility(true); + s_touchCursorHidden = false; + } deskMouseMove(static_cast(msg.wParam), static_cast(msg.lParam)); break; @@ -1119,6 +1133,10 @@ void MSWindowsDesks::deskThread(void *vdesk) break; case DESKFLOW_MSG_FAKE_TOUCH: + // Hide cursor before injection to emulate normal Windows touch behavior: + // touch hides cursor, cursor reappears on next mouse movement. + setCursorVisibility(false); + s_touchCursorHidden = true; deskFakeTouchClick(static_cast(msg.wParam), static_cast(msg.lParam)); break; From 4e410aa9bb993ece36f216beaa73058cbaa18c8d Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 12:44:25 +0000 Subject: [PATCH 10/25] fix: enhance touch injection handling and cursor visibility for non-primary screens --- src/lib/platform/MSWindowsDesks.cpp | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index baac60aab..65bfff06f 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -477,9 +477,10 @@ LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM w SInt32 y = self->m_pendingTouchY; if (self->m_isOnScreen) { self->m_pendingTouchUp = false; + self->m_touchLifted = false; SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); - LOG((CLOG_DEBUG "touch: finger up, hider hidden, injecting at %d,%d", x, y)); + LOG((CLOG_DEBUG "touch: finger up after enter, injecting at %d,%d", x, y)); PostThreadMessage(GetCurrentThreadId(), DESKFLOW_MSG_FAKE_TOUCH, static_cast(x), static_cast(y)); } else { @@ -566,8 +567,16 @@ void MSWindowsDesks::deskFakeTouchClick(SInt32 x, SInt32 y) const } static bool touchInitialized = false; - if (!touchInitialized) { + static bool touchInitAttempted = false; + if (!touchInitAttempted) { + touchInitAttempted = true; touchInitialized = InitializeTouchInjection(2, TOUCH_FEEDBACK_NONE) != 0; + if (touchInitialized) { + LOG((CLOG_DEBUG "touch: InitializeTouchInjection succeeded")); + } else { + LOG((CLOG_WARN "touch: InitializeTouchInjection failed, error=%lu, will use mouse fallback", + GetLastError())); + } } if (touchInitialized) { @@ -1133,10 +1142,14 @@ void MSWindowsDesks::deskThread(void *vdesk) break; case DESKFLOW_MSG_FAKE_TOUCH: - // Hide cursor before injection to emulate normal Windows touch behavior: - // touch hides cursor, cursor reappears on next mouse movement. - setCursorVisibility(false); - s_touchCursorHidden = true; + // On non-primary (client), hide cursor to emulate Windows touch behavior: + // touch hides cursor, cursor reappears on next FAKE_MOVE from server. + // On primary, don't hide — FAKE_MOVE never arrives (real mouse input), + // so the cursor would stay hidden. + if (!m_isPrimary) { + setCursorVisibility(false); + s_touchCursorHidden = true; + } deskFakeTouchClick(static_cast(msg.wParam), static_cast(msg.lParam)); break; From a60420b07f8dd71ffe54ada418cc21c42e335911 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 12:56:15 +0000 Subject: [PATCH 11/25] fix: remove unused cursor visibility handling for touch injection --- src/lib/platform/MSWindowsDesks.cpp | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index 65bfff06f..e51df53b3 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -98,10 +98,6 @@ typedef LONG NTSTATUS; #define DESKFLOW_MSG_TOUCH_UPDATE DESKFLOW_HOOK_LAST_MSG + 14 #define DESKFLOW_MSG_TOUCH_UP DESKFLOW_HOOK_LAST_MSG + 15 -// true when cursor is hidden due to touch injection — show on next mouse move. -// accessed only from the desk thread, so no synchronization needed. -static bool s_touchCursorHidden = false; - static void send_keyboard_input(WORD wVk, WORD wScan, DWORD dwFlags) { INPUT inp; @@ -687,7 +683,6 @@ void setCursorVisibility(bool visible) void MSWindowsDesks::deskEnter(Desk *desk) { - s_touchCursorHidden = false; registerTouchRawInput(desk->m_window, false); if (!m_isPrimary) { @@ -747,7 +742,6 @@ void MSWindowsDesks::deskEnter(Desk *desk) void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) { - s_touchCursorHidden = false; setCursorVisibility(false); if (m_isPrimary) { @@ -1130,10 +1124,6 @@ void MSWindowsDesks::deskThread(void *vdesk) break; case DESKFLOW_MSG_FAKE_MOVE: - if (s_touchCursorHidden) { - setCursorVisibility(true); - s_touchCursorHidden = false; - } deskMouseMove(static_cast(msg.wParam), static_cast(msg.lParam)); break; @@ -1142,14 +1132,6 @@ void MSWindowsDesks::deskThread(void *vdesk) break; case DESKFLOW_MSG_FAKE_TOUCH: - // On non-primary (client), hide cursor to emulate Windows touch behavior: - // touch hides cursor, cursor reappears on next FAKE_MOVE from server. - // On primary, don't hide — FAKE_MOVE never arrives (real mouse input), - // so the cursor would stay hidden. - if (!m_isPrimary) { - setCursorVisibility(false); - s_touchCursorHidden = true; - } deskFakeTouchClick(static_cast(msg.wParam), static_cast(msg.lParam)); break; From d83d09034e545b374cab7fc4707e5b74cc323b8f Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 13:42:18 +0000 Subject: [PATCH 12/25] fix: improve touch detection handling for primary and secondary screens --- src/lib/platform/MSWindowsDesks.cpp | 10 ++++++++-- src/lib/platform/MSWindowsHook.cpp | 6 +++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index e51df53b3..dbd3bce31 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -805,7 +805,11 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) } } - registerTouchRawInput(desk->m_window, true); + // RIDEV_INPUTSINK removed: it consumes raw HID digitizer data before the + // WM_POINTER pipeline can generate pointer messages, causing ~80% of + // WM_POINTER TOUCH events to be silently dropped system-wide. + // Touch detection on primary relies on the LL hook (TOUCH_SIGNATURE) + // and WM_POINTER on the screen window instead. } else { desk->m_foregroundWindow = getForegroundWindow(); EnableWindow(desk->m_window, TRUE); @@ -824,7 +828,9 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) ARCH->sleep(0.03); deskMouseMove(m_xCenter, m_yCenter); - registerTouchRawInput(desk->m_window, true); + // RIDEV_INPUTSINK removed: see comment above. + // Touch detection on secondary relies on WM_POINTER on the overlay + // window (secondaryDeskProc) and the LL hook (TOUCH_SIGNATURE). } } diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index e80437835..de8d72566 100644 --- a/src/lib/platform/MSWindowsHook.cpp +++ b/src/lib/platform/MSWindowsHook.cpp @@ -593,7 +593,11 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) // must run before the injected check: Windows marks // touch-synthesized mouse events as injected (LLMHF_INJECTED). - if (g_touchActivateScreen && g_mode == kHOOK_RELAY_EVENTS) { + // on primary: only detect touch in relay mode (cursor off-screen). + // on secondary (client): always detect touch — g_mode is never set + // to RELAY on secondary screens, so the mode check alone would + // prevent touch detection from ever firing on the client. + if (g_touchActivateScreen && (g_mode == kHOOK_RELAY_EVENTS || !g_isPrimary)) { bool isTouchEvent = (info->dwExtraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE; if (wParam == WM_LBUTTONDOWN) { From 0d0faa01ca87c5638c1733a0b19774fb71360b97 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 13:55:48 +0000 Subject: [PATCH 13/25] fix: optimize touch injection handling to prevent conflicts during touch events --- src/lib/platform/MSWindowsDesks.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index dbd3bce31..01aa18124 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -694,16 +694,16 @@ void MSWindowsDesks::deskEnter(Desk *desk) } bool touchEnter = false; - if (m_pendingTouchUp && m_touchLifted) { - // Finger already lifted — safe to inject now, no real touch conflict. + if (m_pendingTouchUp) { + // Inject immediately — don't defer to WM_POINTERUP because hiding + // the overlay (below) releases implicit pointer capture, so + // WM_POINTERUP never arrives at the hidden window. + // The injected touch uses a separate pointer ID from the real + // hardware touch, so there's no conflict even if the finger is + // still on the screen. touchEnter = true; m_pendingTouchUp = false; m_touchLifted = false; - } else if (m_pendingTouchUp) { - // Finger still down — defer injection to WM_POINTERUP / HID lift. - // Leave m_pendingTouchUp set so secondaryDeskProc WM_POINTERUP - // (or HID tip-off) triggers injection after the real touch ends. - LOG((CLOG_DEBUG "touch: deskEnter, finger still down, deferring injection")); } SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, From f92a5eae1288bc269f32cb687b06cab42e72cfa0 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 13:59:49 +0000 Subject: [PATCH 14/25] Revert "fix: optimize touch injection handling to prevent conflicts during touch events" This reverts commit 0d0faa01ca87c5638c1733a0b19774fb71360b97. --- src/lib/platform/MSWindowsDesks.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index 01aa18124..dbd3bce31 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -694,16 +694,16 @@ void MSWindowsDesks::deskEnter(Desk *desk) } bool touchEnter = false; - if (m_pendingTouchUp) { - // Inject immediately — don't defer to WM_POINTERUP because hiding - // the overlay (below) releases implicit pointer capture, so - // WM_POINTERUP never arrives at the hidden window. - // The injected touch uses a separate pointer ID from the real - // hardware touch, so there's no conflict even if the finger is - // still on the screen. + if (m_pendingTouchUp && m_touchLifted) { + // Finger already lifted — safe to inject now, no real touch conflict. touchEnter = true; m_pendingTouchUp = false; m_touchLifted = false; + } else if (m_pendingTouchUp) { + // Finger still down — defer injection to WM_POINTERUP / HID lift. + // Leave m_pendingTouchUp set so secondaryDeskProc WM_POINTERUP + // (or HID tip-off) triggers injection after the real touch ends. + LOG((CLOG_DEBUG "touch: deskEnter, finger still down, deferring injection")); } SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, From 5aba33caf75493dffd9f6dbf23bc5b3d51650540 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 14:11:21 +0000 Subject: [PATCH 15/25] fix: implement touch event handling for screen activation and deactivation --- src/lib/platform/MSWindowsDesks.cpp | 18 +++--------------- src/lib/platform/MSWindowsHook.cpp | 14 ++++++++++++++ src/lib/platform/MSWindowsHook.h | 2 ++ src/lib/platform/MSWindowsScreen.cpp | 2 ++ 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index dbd3bce31..83fec2ec3 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -812,25 +812,13 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) // and WM_POINTER on the screen window instead. } else { desk->m_foregroundWindow = getForegroundWindow(); - EnableWindow(desk->m_window, TRUE); - SetClassLongPtr(desk->m_window, GCLP_HCURSOR, reinterpret_cast(m_cursor)); - - LONG_PTR exStyle = GetWindowLongPtr(desk->m_window, GWL_EXSTYLE); - exStyle = (exStyle & ~WS_EX_TRANSPARENT) | WS_EX_LAYERED; - SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle); - - SetWindowPos(desk->m_window, HWND_TOPMOST, m_x, m_y, m_w, m_h, SWP_NOACTIVATE | SWP_SHOWWINDOW); - SetLayeredWindowAttributes(desk->m_window, 0, 1, LWA_ALPHA); - - SetCapture(desk->m_window); + // Keep WS_EX_TRANSPARENT — touch events pass through to apps. + // The LL hook eats real mouse events when off-screen (g_isOnScreen=false) + // and passively detects touch via TOUCH_SIGNATURE for screen switching. ARCH->sleep(0.03); deskMouseMove(m_xCenter, m_yCenter); - - // RIDEV_INPUTSINK removed: see comment above. - // Touch detection on secondary relies on WM_POINTER on the overlay - // window (secondaryDeskProc) and the LL hook (TOUCH_SIGNATURE). } } diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index de8d72566..ac942fc1e 100644 --- a/src/lib/platform/MSWindowsHook.cpp +++ b/src/lib/platform/MSWindowsHook.cpp @@ -44,6 +44,7 @@ static BYTE g_keyState[256] = {0}; static DWORD g_hookThread = 0; static bool g_fakeServerInput = false; static BOOL g_isPrimary = TRUE; +static BOOL g_isOnScreen = TRUE; static bool g_touchActivateScreen = false; MSWindowsHook::MSWindowsHook() @@ -107,6 +108,7 @@ int MSWindowsHook::init(DWORD threadID) // set defaults g_mode = kHOOK_DISABLE; + g_isOnScreen = TRUE; g_zoneSides = 0; g_zoneSize = 0; g_xScreen = 0; @@ -159,6 +161,11 @@ void MSWindowsHook::setIsPrimary(bool primary) g_isPrimary = primary ? TRUE : FALSE; } +void MSWindowsHook::setIsOnScreen(bool onScreen) +{ + g_isOnScreen = onScreen ? TRUE : FALSE; +} + static void keyboardGetState(BYTE keys[256], DWORD vkCode, bool kf_up) { // we have to use GetAsyncKeyState() rather than GetKeyState() because @@ -622,6 +629,13 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) return CallNextHookEx(g_mouseLL, code, wParam, lParam); } + // on secondary, eat real mouse events when off-screen to prevent + // interaction while cursor is on server. touch-synthesized events + // (LLMHF_INJECTED) were already allowed through above. + if (!g_isPrimary && !g_isOnScreen) { + return 1; + } + SInt32 x = static_cast(info->pt.x); SInt32 y = static_cast(info->pt.y); SInt32 w = static_cast(HIWORD(info->mouseData)); diff --git a/src/lib/platform/MSWindowsHook.h b/src/lib/platform/MSWindowsHook.h index 8aac00950..ff1bc5c51 100644 --- a/src/lib/platform/MSWindowsHook.h +++ b/src/lib/platform/MSWindowsHook.h @@ -53,4 +53,6 @@ class MSWindowsHook void setTouchActivateScreen(bool enabled); void setIsPrimary(bool primary); + + void setIsOnScreen(bool onScreen); }; diff --git a/src/lib/platform/MSWindowsScreen.cpp b/src/lib/platform/MSWindowsScreen.cpp index 79e45149c..84fd41ea7 100644 --- a/src/lib/platform/MSWindowsScreen.cpp +++ b/src/lib/platform/MSWindowsScreen.cpp @@ -319,6 +319,7 @@ void MSWindowsScreen::enter() // now on screen m_isOnScreen = true; + m_hook.setIsOnScreen(true); setupMouseKeys(); } @@ -375,6 +376,7 @@ void MSWindowsScreen::leave() // now off screen m_isOnScreen = false; + m_hook.setIsOnScreen(false); if (isDraggingStarted() && !m_isPrimary) { m_sendDragThread = new Thread(new TMethodJob(this, &MSWindowsScreen::sendDragThread)); From 455b93674bbb8f3816090de4446b120155b959e5 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 14:43:51 +0000 Subject: [PATCH 16/25] fix: enhance cursor handling for touch events in secondary desk processing --- src/lib/platform/MSWindowsDesks.cpp | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index 83fec2ec3..73d2c4695 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -438,6 +438,10 @@ void setCursorVisibility(bool visible); LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { + case WM_SETCURSOR: + SetCursor(NULL); + return TRUE; + case WM_POINTERACTIVATE: return PA_NOACTIVATE; @@ -813,9 +817,19 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) } else { desk->m_foregroundWindow = getForegroundWindow(); - // Keep WS_EX_TRANSPARENT — touch events pass through to apps. - // The LL hook eats real mouse events when off-screen (g_isOnScreen=false) - // and passively detects touch via TOUCH_SIGNATURE for screen switching. + // Remove WS_EX_TRANSPARENT on a 1x1 window at the cursor center so + // it receives WM_SETCURSOR and hides the cursor (secondaryDeskProc + // returns SetCursor(NULL)). The LL hook eats real mouse events when + // off-screen, pinning the cursor at center — so 1x1 is enough. + // Touch events at any other position pass through to apps. + LONG_PTR exStyle = GetWindowLongPtr(desk->m_window, GWL_EXSTYLE); + exStyle &= ~WS_EX_TRANSPARENT; + SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle); + + SetWindowPos( + desk->m_window, HWND_TOPMOST, m_xCenter, m_yCenter, 1, 1, + SWP_NOACTIVATE | SWP_SHOWWINDOW + ); ARCH->sleep(0.03); deskMouseMove(m_xCenter, m_yCenter); From 5f0978a512c63c5e47d529313d13418a6815f164 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 14:49:16 +0000 Subject: [PATCH 17/25] fix: prevent mouse events from being processed on secondary screens when off-screen --- src/lib/platform/MSWindowsHook.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index ac942fc1e..f58d940f8 100644 --- a/src/lib/platform/MSWindowsHook.cpp +++ b/src/lib/platform/MSWindowsHook.cpp @@ -624,18 +624,19 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) } } + // on secondary when off-screen, eat ALL mouse events (including + // touch-synthesized/injected). WM_POINTER reaches apps directly + // via the window system; letting synthesized mouse events through + // would move the cursor and make it visible. + if (!g_isPrimary && !g_isOnScreen) { + return 1; + } + bool const injected = info->flags & LLMHF_INJECTED; if (!g_isPrimary && injected) { return CallNextHookEx(g_mouseLL, code, wParam, lParam); } - // on secondary, eat real mouse events when off-screen to prevent - // interaction while cursor is on server. touch-synthesized events - // (LLMHF_INJECTED) were already allowed through above. - if (!g_isPrimary && !g_isOnScreen) { - return 1; - } - SInt32 x = static_cast(info->pt.x); SInt32 y = static_cast(info->pt.y); SInt32 w = static_cast(HIWORD(info->mouseData)); From 31c9be1ba70a82da22f55e02cb5913abc133d3dd Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 14:51:49 +0000 Subject: [PATCH 18/25] fix: simplify mouse event handling logic for off-screen scenarios --- src/lib/platform/MSWindowsHook.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index f58d940f8..2c2a9f8f3 100644 --- a/src/lib/platform/MSWindowsHook.cpp +++ b/src/lib/platform/MSWindowsHook.cpp @@ -624,11 +624,11 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) } } - // on secondary when off-screen, eat ALL mouse events (including - // touch-synthesized/injected). WM_POINTER reaches apps directly - // via the window system; letting synthesized mouse events through - // would move the cursor and make it visible. - if (!g_isPrimary && !g_isOnScreen) { + // when off-screen, eat ALL mouse events (including touch-synthesized/ + // injected). WM_POINTER reaches apps directly via the window system; + // letting synthesized mouse events through would move the cursor and + // make it visible. + if (!g_isOnScreen) { return 1; } From 554ef09bf606f8344a47c433f70e698dc861b63d Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 14:55:14 +0000 Subject: [PATCH 19/25] Revert "fix: simplify mouse event handling logic for off-screen scenarios" This reverts commit 31c9be1ba70a82da22f55e02cb5913abc133d3dd. --- src/lib/platform/MSWindowsHook.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index 2c2a9f8f3..f58d940f8 100644 --- a/src/lib/platform/MSWindowsHook.cpp +++ b/src/lib/platform/MSWindowsHook.cpp @@ -624,11 +624,11 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) } } - // when off-screen, eat ALL mouse events (including touch-synthesized/ - // injected). WM_POINTER reaches apps directly via the window system; - // letting synthesized mouse events through would move the cursor and - // make it visible. - if (!g_isOnScreen) { + // on secondary when off-screen, eat ALL mouse events (including + // touch-synthesized/injected). WM_POINTER reaches apps directly + // via the window system; letting synthesized mouse events through + // would move the cursor and make it visible. + if (!g_isPrimary && !g_isOnScreen) { return 1; } From 0fa7b7de419f061aa03a3487da02019014a0ae48 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 15:19:04 +0000 Subject: [PATCH 20/25] fix: enhance mouse event handling for touch-synthesized events when off-screen --- src/lib/platform/MSWindowsHook.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index f58d940f8..5e17889fc 100644 --- a/src/lib/platform/MSWindowsHook.cpp +++ b/src/lib/platform/MSWindowsHook.cpp @@ -624,12 +624,20 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) } } - // on secondary when off-screen, eat ALL mouse events (including - // touch-synthesized/injected). WM_POINTER reaches apps directly - // via the window system; letting synthesized mouse events through - // would move the cursor and make it visible. - if (!g_isPrimary && !g_isOnScreen) { - return 1; + // When off-screen, prevent touch-synthesized mouse events from + // moving/showing the cursor. On secondary, eat ALL mouse events + // (the LL hook replaces SetCapture for mouse containment). + // On primary, only eat touch-synthesized events — regular mouse + // events must still reach mouseHookHandler for relay/switching. + if (!g_isOnScreen) { + if (!g_isPrimary) { + return 1; + } + bool isTouchSynthesized = + (info->dwExtraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE; + if (isTouchSynthesized) { + return 1; + } } bool const injected = info->flags & LLMHF_INJECTED; From c1abc87ddb66729ef842c922c907890040742256 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 15:28:54 +0000 Subject: [PATCH 21/25] Revert "fix: enhance mouse event handling for touch-synthesized events when off-screen" This reverts commit 0fa7b7de419f061aa03a3487da02019014a0ae48. --- src/lib/platform/MSWindowsHook.cpp | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index 5e17889fc..f58d940f8 100644 --- a/src/lib/platform/MSWindowsHook.cpp +++ b/src/lib/platform/MSWindowsHook.cpp @@ -624,20 +624,12 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) } } - // When off-screen, prevent touch-synthesized mouse events from - // moving/showing the cursor. On secondary, eat ALL mouse events - // (the LL hook replaces SetCapture for mouse containment). - // On primary, only eat touch-synthesized events — regular mouse - // events must still reach mouseHookHandler for relay/switching. - if (!g_isOnScreen) { - if (!g_isPrimary) { - return 1; - } - bool isTouchSynthesized = - (info->dwExtraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE; - if (isTouchSynthesized) { - return 1; - } + // on secondary when off-screen, eat ALL mouse events (including + // touch-synthesized/injected). WM_POINTER reaches apps directly + // via the window system; letting synthesized mouse events through + // would move the cursor and make it visible. + if (!g_isPrimary && !g_isOnScreen) { + return 1; } bool const injected = info->flags & LLMHF_INJECTED; From d4d53b5af7ab271a5d9a1353016cbf63b2d79955 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 15:44:20 +0000 Subject: [PATCH 22/25] feat: enhance desk entry handling for touch-triggered events --- src/lib/platform/MSWindowsDesks.cpp | 22 +++++++++++++++------- src/lib/platform/MSWindowsDesks.h | 4 ++-- src/lib/platform/MSWindowsScreen.cpp | 5 ++++- src/lib/platform/MSWindowsScreen.h | 1 + 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index 73d2c4695..2cd6fb868 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -201,9 +201,9 @@ void MSWindowsDesks::disable() m_isOnScreen = m_isPrimary; } -void MSWindowsDesks::enter() +void MSWindowsDesks::enter(bool touchTriggered) { - sendMessage(DESKFLOW_MSG_ENTER, 0, 0); + sendMessage(DESKFLOW_MSG_ENTER, touchTriggered ? 1 : 0, 0); } void MSWindowsDesks::leave(HKL keyLayout) @@ -685,7 +685,7 @@ void setCursorVisibility(bool visible) LOG_ERR("unable to set cursor visibility after %d attempts", attempts); } -void MSWindowsDesks::deskEnter(Desk *desk) +void MSWindowsDesks::deskEnter(Desk *desk, bool touchTriggered) { registerTouchRawInput(desk->m_window, false); @@ -713,11 +713,19 @@ void MSWindowsDesks::deskEnter(Desk *desk) SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + // Always restore the ShowCursor counter (deskLeave decremented it). setCursorVisibility(true); - HCURSOR arrow = LoadCursor(NULL, IDC_ARROW); - SetClassLongPtr(desk->m_window, GCLP_HCURSOR, reinterpret_cast(arrow)); - SetCursor(arrow); + if (touchTriggered) { + // Touch-triggered enter: keep cursor invisible. SetCursor(NULL) + // draws nothing; the cursor becomes visible naturally when the + // user moves the mouse (apps set cursor shape via WM_SETCURSOR). + SetCursor(NULL); + } else { + HCURSOR arrow = LoadCursor(NULL, IDC_ARROW); + SetClassLongPtr(desk->m_window, GCLP_HCURSOR, reinterpret_cast(arrow)); + SetCursor(arrow); + } // restore the foreground window // XXX -- this raises the window to the top of the Z-order. we @@ -1111,7 +1119,7 @@ void MSWindowsDesks::deskThread(void *vdesk) case DESKFLOW_MSG_ENTER: m_isOnScreen = true; - deskEnter(desk); + deskEnter(desk, msg.wParam != 0); break; case DESKFLOW_MSG_LEAVE: diff --git a/src/lib/platform/MSWindowsDesks.h b/src/lib/platform/MSWindowsDesks.h index b042c0f21..2df49dfd8 100644 --- a/src/lib/platform/MSWindowsDesks.h +++ b/src/lib/platform/MSWindowsDesks.h @@ -96,7 +96,7 @@ class MSWindowsDesks /*! Prepares a desk for when the cursor enters it. */ - void enter(); + void enter(bool touchTriggered = false); //! Notify of leaving a desk /*! @@ -229,7 +229,7 @@ class MSWindowsDesks void deskMouseMove(SInt32 x, SInt32 y) const; void deskFakeTouchClick(SInt32 x, SInt32 y) const; void deskMouseRelativeMove(SInt32 dx, SInt32 dy) const; - void deskEnter(Desk *desk); + void deskEnter(Desk *desk, bool touchTriggered = false); void deskLeave(Desk *desk, HKL keyLayout); void deskThread(void *vdesk); diff --git a/src/lib/platform/MSWindowsScreen.cpp b/src/lib/platform/MSWindowsScreen.cpp index 84fd41ea7..20ed80a3f 100644 --- a/src/lib/platform/MSWindowsScreen.cpp +++ b/src/lib/platform/MSWindowsScreen.cpp @@ -294,7 +294,9 @@ void MSWindowsScreen::disable() void MSWindowsScreen::enter() { - m_desks->enter(); + bool touchEnter = m_touchTriggeredEnter; + m_touchTriggeredEnter = false; + m_desks->enter(touchEnter); if (m_isPrimary) { // enable special key sequences on win95 family enableSpecialKeys(true); @@ -992,6 +994,7 @@ bool MSWindowsScreen::onPreDispatch(HWND hwnd, UINT message, WPARAM wParam, LPAR SInt32 y = static_cast(lParam); if (m_isPrimary) { LOG((CLOG_INFO "hook: touch activating primary at %d,%d", x, y)); + m_touchTriggeredEnter = true; sendEvent(m_events->forIPrimaryScreen().touchActivatedPrimary(), MotionInfo::alloc(x, y)); } else { diff --git a/src/lib/platform/MSWindowsScreen.h b/src/lib/platform/MSWindowsScreen.h index 6aa31816f..7b575f2b6 100644 --- a/src/lib/platform/MSWindowsScreen.h +++ b/src/lib/platform/MSWindowsScreen.h @@ -364,6 +364,7 @@ class MSWindowsScreen : public PlatformScreen MSWindowsPowerManager m_powerManager; bool m_touchActivateScreen; + bool m_touchTriggeredEnter = false; Stopwatch m_touchDebounceTimer; static constexpr double kTouchDebounceTime = 0.15; From db4939244484fa91527a5754280c1652eae72229 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 15:45:57 +0000 Subject: [PATCH 23/25] Revert "feat: enhance desk entry handling for touch-triggered events" This reverts commit d4d53b5af7ab271a5d9a1353016cbf63b2d79955. --- src/lib/platform/MSWindowsDesks.cpp | 22 +++++++--------------- src/lib/platform/MSWindowsDesks.h | 4 ++-- src/lib/platform/MSWindowsScreen.cpp | 5 +---- src/lib/platform/MSWindowsScreen.h | 1 - 4 files changed, 10 insertions(+), 22 deletions(-) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index 2cd6fb868..73d2c4695 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -201,9 +201,9 @@ void MSWindowsDesks::disable() m_isOnScreen = m_isPrimary; } -void MSWindowsDesks::enter(bool touchTriggered) +void MSWindowsDesks::enter() { - sendMessage(DESKFLOW_MSG_ENTER, touchTriggered ? 1 : 0, 0); + sendMessage(DESKFLOW_MSG_ENTER, 0, 0); } void MSWindowsDesks::leave(HKL keyLayout) @@ -685,7 +685,7 @@ void setCursorVisibility(bool visible) LOG_ERR("unable to set cursor visibility after %d attempts", attempts); } -void MSWindowsDesks::deskEnter(Desk *desk, bool touchTriggered) +void MSWindowsDesks::deskEnter(Desk *desk) { registerTouchRawInput(desk->m_window, false); @@ -713,19 +713,11 @@ void MSWindowsDesks::deskEnter(Desk *desk, bool touchTriggered) SetWindowPos(desk->m_window, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); - // Always restore the ShowCursor counter (deskLeave decremented it). setCursorVisibility(true); - if (touchTriggered) { - // Touch-triggered enter: keep cursor invisible. SetCursor(NULL) - // draws nothing; the cursor becomes visible naturally when the - // user moves the mouse (apps set cursor shape via WM_SETCURSOR). - SetCursor(NULL); - } else { - HCURSOR arrow = LoadCursor(NULL, IDC_ARROW); - SetClassLongPtr(desk->m_window, GCLP_HCURSOR, reinterpret_cast(arrow)); - SetCursor(arrow); - } + HCURSOR arrow = LoadCursor(NULL, IDC_ARROW); + SetClassLongPtr(desk->m_window, GCLP_HCURSOR, reinterpret_cast(arrow)); + SetCursor(arrow); // restore the foreground window // XXX -- this raises the window to the top of the Z-order. we @@ -1119,7 +1111,7 @@ void MSWindowsDesks::deskThread(void *vdesk) case DESKFLOW_MSG_ENTER: m_isOnScreen = true; - deskEnter(desk, msg.wParam != 0); + deskEnter(desk); break; case DESKFLOW_MSG_LEAVE: diff --git a/src/lib/platform/MSWindowsDesks.h b/src/lib/platform/MSWindowsDesks.h index 2df49dfd8..b042c0f21 100644 --- a/src/lib/platform/MSWindowsDesks.h +++ b/src/lib/platform/MSWindowsDesks.h @@ -96,7 +96,7 @@ class MSWindowsDesks /*! Prepares a desk for when the cursor enters it. */ - void enter(bool touchTriggered = false); + void enter(); //! Notify of leaving a desk /*! @@ -229,7 +229,7 @@ class MSWindowsDesks void deskMouseMove(SInt32 x, SInt32 y) const; void deskFakeTouchClick(SInt32 x, SInt32 y) const; void deskMouseRelativeMove(SInt32 dx, SInt32 dy) const; - void deskEnter(Desk *desk, bool touchTriggered = false); + void deskEnter(Desk *desk); void deskLeave(Desk *desk, HKL keyLayout); void deskThread(void *vdesk); diff --git a/src/lib/platform/MSWindowsScreen.cpp b/src/lib/platform/MSWindowsScreen.cpp index 20ed80a3f..84fd41ea7 100644 --- a/src/lib/platform/MSWindowsScreen.cpp +++ b/src/lib/platform/MSWindowsScreen.cpp @@ -294,9 +294,7 @@ void MSWindowsScreen::disable() void MSWindowsScreen::enter() { - bool touchEnter = m_touchTriggeredEnter; - m_touchTriggeredEnter = false; - m_desks->enter(touchEnter); + m_desks->enter(); if (m_isPrimary) { // enable special key sequences on win95 family enableSpecialKeys(true); @@ -994,7 +992,6 @@ bool MSWindowsScreen::onPreDispatch(HWND hwnd, UINT message, WPARAM wParam, LPAR SInt32 y = static_cast(lParam); if (m_isPrimary) { LOG((CLOG_INFO "hook: touch activating primary at %d,%d", x, y)); - m_touchTriggeredEnter = true; sendEvent(m_events->forIPrimaryScreen().touchActivatedPrimary(), MotionInfo::alloc(x, y)); } else { diff --git a/src/lib/platform/MSWindowsScreen.h b/src/lib/platform/MSWindowsScreen.h index 7b575f2b6..6aa31816f 100644 --- a/src/lib/platform/MSWindowsScreen.h +++ b/src/lib/platform/MSWindowsScreen.h @@ -364,7 +364,6 @@ class MSWindowsScreen : public PlatformScreen MSWindowsPowerManager m_powerManager; bool m_touchActivateScreen; - bool m_touchTriggeredEnter = false; Stopwatch m_touchDebounceTimer; static constexpr double kTouchDebounceTime = 0.15; From d642585bb9c4fa3db83a9738cbd83f247e324f00 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 15:54:21 +0000 Subject: [PATCH 24/25] feat: remove fake touch click from primary event handling --- src/lib/server/Server.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index e52ff4e68..a7d51189f 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -1383,7 +1383,6 @@ void Server::handleTouchActivatedPrimaryEvent(const Event &event, void *) switchScreen(m_primaryClient, x, y, false); m_primaryClient->activateWindowAt(x, y); - m_primaryClient->fakeTouchClick(x, y); m_touchSwitchCooldown.reset(); LOG((CLOG_DEBUG1 "touch switch cooldown started")); From 7493cf30dd2e99231cfe088afc4e83e6f622e4b6 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Wed, 4 Mar 2026 16:40:47 +0000 Subject: [PATCH 25/25] fix: prevent cursor visibility by eating injected mouse events on secondary off-screen --- src/lib/platform/MSWindowsHook.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index f58d940f8..da2a23ea3 100644 --- a/src/lib/platform/MSWindowsHook.cpp +++ b/src/lib/platform/MSWindowsHook.cpp @@ -624,15 +624,17 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) } } - // on secondary when off-screen, eat ALL mouse events (including - // touch-synthesized/injected). WM_POINTER reaches apps directly - // via the window system; letting synthesized mouse events through - // would move the cursor and make it visible. - if (!g_isPrimary && !g_isOnScreen) { + bool const injected = info->flags & LLMHF_INJECTED; + + // On secondary when off-screen, eat injected mouse events (which + // includes touch-synthesized ones marked LLMHF_INJECTED). This + // prevents the cursor from jumping to the touch point and becoming + // visible. Local (non-injected) mouse events pass through so the + // cursor can move normally (hidden via ShowCursor counter). + if (!g_isPrimary && !g_isOnScreen && injected) { return 1; } - bool const injected = info->flags & LLMHF_INJECTED; if (!g_isPrimary && injected) { return CallNextHookEx(g_mouseLL, code, wParam, lParam); }