diff --git a/src/gui/src/ServerConfig.cpp b/src/gui/src/ServerConfig.cpp index 39d2aa8e6..cb39b6764 100644 --- a/src/gui/src/ServerConfig.cpp +++ b/src/gui/src/ServerConfig.cpp @@ -79,7 +79,8 @@ bool ServerConfig::operator==(const ServerConfig &sc) const m_HasSwitchDoubleTap == sc.m_HasSwitchDoubleTap && m_SwitchDoubleTap == sc.m_SwitchDoubleTap && 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_DisableLockToScreen == sc.m_DisableLockToScreen && m_TouchActivateScreen == sc.m_TouchActivateScreen && + m_ClipboardSharing == sc.m_ClipboardSharing && m_ClipboardSharingSize == sc.m_ClipboardSharingSize && m_pMainWindow == sc.m_pMainWindow; } @@ -125,6 +126,7 @@ void ServerConfig::commit() settings().setValue("switchDoubleTap", switchDoubleTap()); settings().setValue("switchCornerSize", switchCornerSize()); settings().setValue("disableLockToScreen", disableLockToScreen()); + settings().setValue("touchActivateScreen", touchActivateScreen()); settings().setValue("clipboardSharing", clipboardSharing()); settings().setValue("clipboardSharingSize", QVariant::fromValue(clipboardSharingSize())); @@ -178,6 +180,7 @@ void ServerConfig::recall() setSwitchDoubleTap(settings().value("switchDoubleTap", 250).toInt()); setSwitchCornerSize(settings().value("switchCornerSize").toInt()); setDisableLockToScreen(settings().value("disableLockToScreen", false).toBool()); + setTouchActivateScreen(settings().value("touchActivateScreen", false).toBool()); setClipboardSharingSize( settings().value("clipboardSharingSize", (int)ServerConfig::defaultClipboardSharingSize()).toULongLong() ); @@ -282,6 +285,8 @@ QTextStream &operator<<(QTextStream &outStream, const ServerConfig &config) << "win32KeepForeground = " << (config.win32KeepForeground() ? "true" : "false") << Qt::endl; outStream << "\t" << "disableLockToScreen = " << (config.disableLockToScreen() ? "true" : "false") << Qt::endl; + outStream << "\t" + << "touchActivateScreen = " << (config.touchActivateScreen() ? "true" : "false") << Qt::endl; outStream << "\t" << "clipboardSharing = " << (config.clipboardSharing() ? "true" : "false") << Qt::endl; outStream << "\t" diff --git a/src/gui/src/ServerConfig.h b/src/gui/src/ServerConfig.h index 2c99fa7fd..0219d461c 100644 --- a/src/gui/src/ServerConfig.h +++ b/src/gui/src/ServerConfig.h @@ -120,6 +120,10 @@ class ServerConfig : public ScreenConfig, public deskflow::gui::IServerConfig { return m_DisableLockToScreen; } + bool touchActivateScreen() const + { + return m_TouchActivateScreen; + } bool clipboardSharing() const { return m_ClipboardSharing; @@ -220,6 +224,10 @@ class ServerConfig : public ScreenConfig, public deskflow::gui::IServerConfig { m_DisableLockToScreen = on; } + void setTouchActivateScreen(bool on) + { + m_TouchActivateScreen = on; + } void setClipboardSharing(bool on) { m_ClipboardSharing = on; @@ -252,6 +260,7 @@ class ServerConfig : public ScreenConfig, public deskflow::gui::IServerConfig int m_SwitchDoubleTap = 0; int m_SwitchCornerSize = 0; bool m_DisableLockToScreen = false; + bool m_TouchActivateScreen = false; bool m_ClipboardSharing = true; QString m_ClientAddress = ""; QList m_SwitchCorners; 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..d24fce7f1 100644 --- a/src/gui/src/ServerConfigDialogBase.ui +++ b/src/gui/src/ServerConfigDialogBase.ui @@ -798,6 +798,13 @@ + + + + Touch screen to activate (switch by tapping touchscreen) + + + @@ -1168,6 +1175,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..cbd20b53a 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, grabInput) // // 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, grabInput) // // IpcServer diff --git a/src/lib/base/EventTypes.h b/src/lib/base/EventTypes.h index 6ddfe8adf..288dea80e 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_grabInput(Event::kUnknown) { } @@ -410,11 +410,15 @@ class ClientProxyEvents : public EventTypes */ Event::Type disconnected(); + //! Get grab input event type + Event::Type grabInput(); + //@} private: Event::Type m_ready; Event::Type m_disconnected; + Event::Type m_grabInput; }; class ClientProxyUnknownEvents : public EventTypes @@ -603,7 +607,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 +655,9 @@ class IPrimaryScreenEvents : public EventTypes //! end of fake input event type Event::Type fakeInputEnd(); + //! touch activated primary screen event type + Event::Type touchActivatedPrimary(); + //@} private: @@ -664,6 +672,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 +682,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_grabInput(Event::kUnknown) { } @@ -708,6 +718,9 @@ class IScreenEvents : public EventTypes */ Event::Type resume(); + //! Get grab input event type + Event::Type grabInput(); + //@} private: @@ -715,6 +728,7 @@ class IScreenEvents : public EventTypes Event::Type m_shapeChanged; Event::Type m_suspend; Event::Type m_resume; + Event::Type m_grabInput; }; class ClipboardEvents : public EventTypes diff --git a/src/lib/client/Client.cpp b/src/lib/client/Client.cpp index d23077f6b..ab46dc23b 100644 --- a/src/lib/client/Client.cpp +++ b/src/lib/client/Client.cpp @@ -87,6 +87,10 @@ Client::Client( m_events->adoptHandler( m_events->forIScreen().resume(), getEventTarget(), new TMethodEventJob(this, &Client::handleResume) ); + m_events->adoptHandler( + m_events->forIScreen().grabInput(), m_screen->getEventTarget(), + new TMethodEventJob(this, &Client::handleGrabInput) + ); if (m_args.m_enableDragDrop) { m_events->adoptHandler( @@ -107,6 +111,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().grabInput(), m_screen->getEventTarget()); cleanupTimer(); cleanupScreen(); @@ -240,6 +245,13 @@ void Client::enter(SInt32 xAbs, SInt32 yAbs, UInt32, KeyModifierMask mask, bool) m_screen->mouseMove(xAbs, yAbs); m_screen->enter(mask); + SInt32 tx, ty; + if (m_screen->consumePendingTouchActivation(tx, ty)) { + LOG((CLOG_DEBUG "client enter: consuming pending touch at %d,%d", tx, ty)); + m_screen->activateWindowAt(tx, ty); + m_screen->fakeTouchClick(tx, ty); + } + if (m_sendFileThread) { StreamChunker::interruptFile(); m_sendFileThread.reset(nullptr); @@ -719,6 +731,16 @@ void Client::handleResume(const Event &, void *) } } +void Client::handleGrabInput(const Event &event, void *) +{ + IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); + if (m_server != NULL) { + LOG((CLOG_DEBUG "requesting grab input at %d,%d", info->m_x, info->m_y)); + m_screen->setPendingTouchActivation(info->m_x, info->m_y); + m_server->grabInput(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..e8b6caa21 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 handleGrabInput(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..76d9ae226 100644 --- a/src/lib/client/ServerProxy.cpp +++ b/src/lib/client/ServerProxy.cpp @@ -380,6 +380,12 @@ void ServerProxy::onClipboardChanged(ClipboardID id, const IClipboard *clipboard StreamChunker::sendClipboard(data, data.size(), id, m_seqNum, m_events, this); } +void ServerProxy::grabInput(SInt32 x, SInt32 y) +{ + LOG((CLOG_DEBUG "sending grab input at %d,%d", x, y)); + ProtocolUtil::writef(m_stream, kMsgCGrabInput, x, y); +} + void ServerProxy::flushCompressedMouse() { if (m_compressMouse) { diff --git a/src/lib/client/ServerProxy.h b/src/lib/client/ServerProxy.h index 743c3393f..18a789594 100644 --- a/src/lib/client/ServerProxy.h +++ b/src/lib/client/ServerProxy.h @@ -61,6 +61,8 @@ class ServerProxy bool onGrabClipboard(ClipboardID); void onClipboardChanged(ClipboardID, const IClipboard *); + void grabInput(SInt32 x, SInt32 y); + //@} // sending file chunk to server diff --git a/src/lib/deskflow/IPlatformScreen.h b/src/lib/deskflow/IPlatformScreen.h index 1ca2ceea2..23e9797f4 100644 --- a/src/lib/deskflow/IPlatformScreen.h +++ b/src/lib/deskflow/IPlatformScreen.h @@ -200,6 +200,12 @@ class IPlatformScreen : public IScreen, public IPrimaryScreen, public ISecondary virtual void pollPressedKeys(KeyButtonSet &pressedKeys) const = 0; virtual void clearStaleModifiers() = 0; + // Touch-to-switch overrides + virtual void activateWindowAt(SInt32 x, SInt32 y) {} + virtual void fakeTouchClick(SInt32 x, SInt32 y) {} + virtual void setPendingTouchActivation(SInt32 x, SInt32 y) { (void)x; (void)y; } + virtual bool consumePendingTouchActivation(SInt32 &x, SInt32 &y) { (void)x; (void)y; return false; } + // 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..cf384332b 100644 --- a/src/lib/deskflow/Screen.cpp +++ b/src/lib/deskflow/Screen.cpp @@ -221,6 +221,26 @@ void Screen::keyUp(KeyID, KeyModifierMask, KeyButton button) m_screen->fakeKeyUp(button); } +void Screen::activateWindowAt(SInt32 x, SInt32 y) +{ + m_screen->activateWindowAt(x, y); +} + +void Screen::fakeTouchClick(SInt32 x, SInt32 y) +{ + m_screen->fakeTouchClick(x, y); +} + +void Screen::setPendingTouchActivation(SInt32 x, SInt32 y) +{ + m_screen->setPendingTouchActivation(x, y); +} + +bool Screen::consumePendingTouchActivation(SInt32 &x, SInt32 &y) +{ + return m_screen->consumePendingTouchActivation(x, y); +} + void Screen::mouseDown(ButtonID button) { m_screen->fakeMouseButton(button, true); diff --git a/src/lib/deskflow/Screen.h b/src/lib/deskflow/Screen.h index 9e4c3ef63..70cd69e77 100644 --- a/src/lib/deskflow/Screen.h +++ b/src/lib/deskflow/Screen.h @@ -146,6 +146,18 @@ class Screen : public IScreen */ void keyUp(KeyID id, KeyModifierMask, KeyButton); + //! Activate the window at the given coordinates + void activateWindowAt(SInt32 x, SInt32 y); + + //! Inject a touch click at the given coordinates + void fakeTouchClick(SInt32 x, SInt32 y); + + //! Store coordinates for deferred touch activation after screen switch + void setPendingTouchActivation(SInt32 x, SInt32 y); + + //! Consume pending touch activation, returning true if one was pending + bool consumePendingTouchActivation(SInt32 &x, SInt32 &y); + //! Notify of mouse press /*! Synthesize mouse events to generate a press of mouse button \c id. 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..65f2afd43 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 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 5c67bb161..e86921779 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 input 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; @@ -294,6 +295,11 @@ extern const char *const kMsgDSecureInputNotification; // $1 = List of server languages extern const char *const kMsgDLanguageSynchronisation; +// 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 kMsgCGrabInput; + // // query codes // diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index c9afc4d31..0235e29bf 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -29,9 +29,11 @@ #include "mt/Lock.h" #include "mt/Thread.h" #include "platform/MSWindowsScreen.h" +#include "platform/MSWindowsTouchInjector.h" #include "platform/dfwhook.h" #include +#include // these are only defined when WINVER >= 0x0500 #if !defined(SPI_GETMOUSESPEED) @@ -86,6 +88,27 @@ #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 + +// suppress first WM_MOUSEMOVE after leaving screen to prevent the hider +// window from being hidden by spurious mouse motion during screen switch. +// accessed only from the desk thread, so no synchronization needed. +static bool s_suppressMouseMove = false; + +// main thread ID for posting messages from the desk thread to the event queue. +// set once from enable() on the main thread. +static DWORD s_mainThreadID = 0; + +// true when touch-to-switch is enabled — set from the main thread, read from desk thread. +static bool s_touchActivateScreen = false; + +// touch injection handler — accessed only from the desk thread. +static MSWindowsTouchInjector s_touchInjector; + +// 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) { @@ -159,6 +182,7 @@ MSWindowsDesks::~MSWindowsDesks() void MSWindowsDesks::enable() { m_threadID = GetCurrentThreadId(); + s_mainThreadID = m_threadID; // set the active desk and (re)install the hooks checkDesk(); @@ -211,6 +235,8 @@ void MSWindowsDesks::setOptions(const OptionsList &options) if (options[i] == kOptionWin32KeepForeground) { m_leaveForegroundOption = (options[i + 1] != 0); LOG((CLOG_DEBUG1 "%s the foreground window", m_leaveForegroundOption ? "don\'t grab" : "grab")); + } else if (options[i] == kOptionTouchActivateScreen) { + s_touchActivateScreen = (options[i + 1] != 0); } } } @@ -331,6 +357,11 @@ void MSWindowsDesks::fakeMouseWheel(SInt32 xDelta, SInt32 yDelta) const sendMessage(DESKFLOW_MSG_FAKE_WHEEL, xDelta, yDelta); } +void MSWindowsDesks::fakeTouchClick(SInt32 x, SInt32 y) const +{ + sendMessage(DESKFLOW_MSG_FAKE_TOUCH, static_cast(x), static_cast(y)); +} + void MSWindowsDesks::sendMessage(UINT msg, WPARAM wParam, LPARAM lParam) const { if (m_activeDesk != NULL && m_activeDesk->m_window != NULL) { @@ -414,20 +445,67 @@ 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; + case WM_POINTERACTIVATE: + // prevent touch from activating the hider window + return PA_NOACTIVATE; + + case WM_POINTERDOWN: { + // Touch detection via modern Windows pointer API. No config check here — + // the hider window only exists when the cursor is off-screen, so any + // touch on it is a "switch back" action. The config gate is in onPreDispatch. + UINT32 pointerId = GET_POINTERID_WPARAM(wParam); + DWORD pointerType = PT_POINTER; + if (GetPointerType(pointerId, &pointerType) && (pointerType == PT_TOUCH || pointerType == PT_PEN)) { + SInt32 x = GET_X_LPARAM(lParam); + SInt32 y = GET_Y_LPARAM(lParam); + LOG((CLOG_DEBUG "secondaryDeskProc: WM_POINTERDOWN touch at %d,%d", x, y)); + PostThreadMessage( + s_mainThreadID, DESKFLOW_MSG_TOUCH, static_cast(x), static_cast(y) + ); + return 0; } break; } - if (hide && IsWindowVisible(hwnd)) { - ReleaseCapture(); - SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + case WM_LBUTTONDOWN: { + // Backup touch detection via legacy mouse message + touch signature. + // SetCapture routes WM_LBUTTONDOWN to the hider window for touches + // that don't arrive via WM_POINTER. + DWORD extraInfo = (DWORD)GetMessageExtraInfo(); + if ((extraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE) { + POINT pt; + GetCursorPos(&pt); + LOG((CLOG_DEBUG "secondaryDeskProc: WM_LBUTTONDOWN touch at %d,%d", pt.x, pt.y)); + PostThreadMessage( + s_mainThreadID, DESKFLOW_MSG_TOUCH, static_cast(pt.x), static_cast(pt.y) + ); + return 0; + } + break; + } + + case WM_MOUSEMOVE: { + if (s_suppressMouseMove) { + s_suppressMouseMove = false; + break; + } + + // filter touch-originated mouse moves — only real mouse motion + // should dismiss the hider window + DWORD extraInfo = (DWORD)GetMessageExtraInfo(); + if ((extraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE) { + break; + } + + if (LOWORD(lParam) != 0 || HIWORD(lParam) != 0) { + if (IsWindowVisible(hwnd)) { + ReleaseCapture(); + SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + } + } + break; + } } return DefWindowProc(hwnd, msg, wParam, lParam); @@ -517,6 +595,9 @@ void setCursorVisibility(bool visible) void MSWindowsDesks::deskEnter(Desk *desk) { + s_suppressMouseMove = false; + s_touchCursorHidden = false; + if (!m_isPrimary) { ReleaseCapture(); } @@ -542,6 +623,7 @@ void MSWindowsDesks::deskEnter(Desk *desk) void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) { + s_touchCursorHidden = false; setCursorVisibility(false); if (m_isPrimary) { @@ -600,6 +682,10 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) } } } else { + // suppress the first mouse move to prevent the hider window from + // being hidden by spurious motion during screen switch + s_suppressMouseMove = true; + // 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); @@ -712,6 +798,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; @@ -754,6 +844,14 @@ void MSWindowsDesks::deskThread(void *vdesk) DESKFLOW_HOOK_FAKE_INPUT_VIRTUAL_KEY, DESKFLOW_HOOK_FAKE_INPUT_SCANCODE, msg.wParam ? 0 : KEYEVENTF_KEYUP ); 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; + s_touchInjector.injectClickAt(static_cast(msg.wParam), static_cast(msg.lParam)); + break; } // notify that message was processed diff --git a/src/lib/platform/MSWindowsDesks.h b/src/lib/platform/MSWindowsDesks.h index eb225a381..c2ea6a42c 100644 --- a/src/lib/platform/MSWindowsDesks.h +++ b/src/lib/platform/MSWindowsDesks.h @@ -187,6 +187,12 @@ class MSWindowsDesks */ void fakeMouseWheel(SInt32 xDelta, SInt32 yDelta) const; + //! Fake touch click + /*! + Activate the window at (\c x, \c y) and inject a touch click there. + */ + void fakeTouchClick(SInt32 x, SInt32 y) const; + //@} private: diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index b51b1a734..5fbc438b6 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_touchActivateScreen = false; MSWindowsHook::MSWindowsHook() { @@ -148,6 +149,19 @@ void MSWindowsHook::setMode(EHookMode mode) g_mode = mode; } +void MSWindowsHook::setTouchActivateScreen(bool enabled) +{ + g_touchActivateScreen = enabled; + LOG((CLOG_DEBUG "hook: touchActivateScreen=%s", enabled ? "true" : "false")); +} + +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 +594,30 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) // decode the message MSLLHOOKSTRUCT *info = reinterpret_cast(lParam); + // detect touch-originated mouse events via the MI_WP_SIGNATURE. + // must run BEFORE the injected check below, because Windows marks + // touch-generated mouse events as LLMHF_INJECTED (they're synthesized + // by the touch subsystem). without this ordering, touch events on + // client screens are silently dropped by the injected early-return. + 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) { + return 1; // eat event on primary + } + } + } + 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..98f1256f4 100644 --- a/src/lib/platform/MSWindowsHook.h +++ b/src/lib/platform/MSWindowsHook.h @@ -42,6 +42,9 @@ class MSWindowsHook void setMode(EHookMode mode); + void setTouchActivateScreen(bool enabled); + void setIsPrimary(bool primary); + static EHookResult install(); static int uninstall(); diff --git a/src/lib/platform/MSWindowsScreen.cpp b/src/lib/platform/MSWindowsScreen.cpp index f585110cf..27787aec8 100644 --- a/src/lib/platform/MSWindowsScreen.cpp +++ b/src/lib/platform/MSWindowsScreen.cpp @@ -30,7 +30,9 @@ #include "deskflow/ArgsBase.h" #include "deskflow/ClientApp.h" #include "deskflow/Clipboard.h" +#include "deskflow/IPrimaryScreen.h" #include "deskflow/KeyMap.h" +#include "deskflow/option_types.h" #include "deskflow/XScreen.h" #include "mt/Thread.h" #include "platform/MSWindowsClipboard.h" @@ -41,6 +43,7 @@ #include "platform/MSWindowsScreenSaver.h" #include +#include #include #include @@ -133,8 +136,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(); @@ -244,6 +248,9 @@ void MSWindowsScreen::enable() // track the active desk and (re)install the hooks m_desks->enable(); + // configure hook for this screen type + m_hook.setTouchActivateScreen(m_touchActivateScreen); + if (m_isPrimary) { // set jump zones m_hook.setZone(m_x, m_y, m_w, m_h, getJumpZoneSize()); @@ -289,6 +296,7 @@ void MSWindowsScreen::disable() void MSWindowsScreen::enter() { m_desks->enter(); + if (m_isPrimary) { // enable special key sequences on win95 family enableSpecialKeys(true); @@ -476,6 +484,14 @@ void MSWindowsScreen::resetOptions() void MSWindowsScreen::setOptions(const OptionsList &options) { m_desks->setOptions(options); + + 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: %s", m_touchActivateScreen ? "enabled" : "disabled")); + } + } } void MSWindowsScreen::setSequenceNumber(UInt32 seqNum) @@ -957,6 +973,37 @@ 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: { + if (!m_touchActivateScreen) { + return true; + } + + SInt32 x = static_cast(wParam); + SInt32 y = static_cast(lParam); + + // debounce rapid touch events + double elapsed = m_touchDebounceTimer.getTime(); + if (elapsed > 0.0 && elapsed < kTouchDebounceTime) { + LOG((CLOG_DEBUG "touch debounced at %d,%d (%.0fms)", x, y, elapsed * 1000)); + return true; + } + m_touchDebounceTimer.reset(); + m_touchDebounceTimer.start(); + + if (m_isPrimary) { + // primary screen: cursor is on another screen, touch here to switch back + LOG((CLOG_DEBUG "touch on primary at %d,%d — firing touchActivatedPrimary", x, y)); + auto *info = IPrimaryScreen::MotionInfo::alloc(x, y); + sendEvent(m_events->forIPrimaryScreen().touchActivatedPrimary(), info); + } else { + // client screen: cursor is on another screen, touch here to grab input + LOG((CLOG_DEBUG "touch on client at %d,%d — firing grabInput", x, y)); + auto *info = IPrimaryScreen::MotionInfo::alloc(x, y); + sendEvent(m_events->forIScreen().grabInput(), info); + } + return true; + } } if (m_isPrimary) { @@ -1043,6 +1090,15 @@ 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)) { + *result = 0; + return true; + } + 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 +1464,51 @@ bool MSWindowsScreen::onScreensaver(bool activated) return true; } +bool MSWindowsScreen::isPointerTypeTouch(UINT32 pointerId) const +{ + DWORD pointerType = PT_POINTER; + if (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; + } + + double elapsed = m_touchDebounceTimer.getTime(); + if (elapsed > 0.0 && elapsed < kTouchDebounceTime) { + LOG((CLOG_DEBUG "WM_POINTER: touch debounced (%.0fms)", elapsed * 1000)); + return true; + } + m_touchDebounceTimer.reset(); + m_touchDebounceTimer.start(); + + SInt32 x = GET_X_LPARAM(lParam); + SInt32 y = GET_Y_LPARAM(lParam); + + if (m_isPrimary) { + LOG((CLOG_DEBUG "WM_POINTER: touch on primary at %d,%d — firing touchActivatedPrimary", x, y)); + auto *info = IPrimaryScreen::MotionInfo::alloc(x, y); + sendEvent(m_events->forIPrimaryScreen().touchActivatedPrimary(), info); + } else { + LOG((CLOG_DEBUG "WM_POINTER: touch on client at %d,%d — firing grabInput", x, y)); + auto *info = IPrimaryScreen::MotionInfo::alloc(x, y); + sendEvent(m_events->forIScreen().grabInput(), info); + } + + return true; +} + bool MSWindowsScreen::onDisplayChange() { // screen resolution may have changed. save old shape. @@ -1899,3 +2000,35 @@ bool MSWindowsScreen::isModifierRepeat(KeyModifierMask oldState, KeyModifierMask return result; } + +void MSWindowsScreen::activateWindowAt(SInt32 x, SInt32 y) +{ + // activation is handled as part of fakeTouchClick via MSWindowsTouchInjector + LOG((CLOG_DEBUG "activateWindowAt %d,%d (handled by fakeTouchClick)", x, y)); +} + +void MSWindowsScreen::fakeTouchClick(SInt32 x, SInt32 y) +{ + LOG((CLOG_DEBUG "fakeTouchClick %d,%d — delegating to desk thread", x, y)); + m_desks->fakeTouchClick(x, y); +} + +void MSWindowsScreen::setPendingTouchActivation(SInt32 x, SInt32 y) +{ + m_touchGrabPending = true; + m_touchGrabX = x; + m_touchGrabY = y; + LOG((CLOG_DEBUG "pending touch activation set at %d,%d", x, y)); +} + +bool MSWindowsScreen::consumePendingTouchActivation(SInt32 &x, SInt32 &y) +{ + if (!m_touchGrabPending) { + return false; + } + m_touchGrabPending = false; + x = m_touchGrabX; + y = m_touchGrabY; + LOG((CLOG_DEBUG "pending touch activation consumed at %d,%d", x, y)); + return true; +} diff --git a/src/lib/platform/MSWindowsScreen.h b/src/lib/platform/MSWindowsScreen.h index 8688d4286..95dc3d4c4 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" @@ -136,6 +137,10 @@ class MSWindowsScreen : public PlatformScreen virtual String &getDraggingFilename(); virtual const String &getDropTarget() const; String getSecureInputApp() const override; + void activateWindowAt(SInt32 x, SInt32 y) override; + void fakeTouchClick(SInt32 x, SInt32 y) override; + void setPendingTouchActivation(SInt32 x, SInt32 y) override; + bool consumePendingTouchActivation(SInt32 &x, SInt32 &y) override; protected: // IPlatformScreen overrides @@ -189,6 +194,8 @@ class MSWindowsScreen : public PlatformScreen bool onMouseWheel(SInt32 xDelta, SInt32 yDelta); bool onScreensaver(bool activated); bool onDisplayChange(); + bool isPointerTypeTouch(UINT32 pointerId) const; + bool onPointerInput(WPARAM wParam, LPARAM lParam); bool onClipboardChange(); // warp cursor without discarding queued events @@ -357,4 +364,14 @@ class MSWindowsScreen : public PlatformScreen PrimaryKeyDownList m_primaryKeyDownList; MSWindowsPowerManager m_powerManager; + + // touch-to-switch + bool m_touchActivateScreen = false; + Stopwatch m_touchDebounceTimer; + static constexpr double kTouchDebounceTime = 0.3; + + // pending touch activation for client-side injection + bool m_touchGrabPending = false; + SInt32 m_touchGrabX = 0; + SInt32 m_touchGrabY = 0; }; diff --git a/src/lib/platform/MSWindowsTouchInjector.cpp b/src/lib/platform/MSWindowsTouchInjector.cpp new file mode 100644 index 000000000..a10b8b16b --- /dev/null +++ b/src/lib/platform/MSWindowsTouchInjector.cpp @@ -0,0 +1,236 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * Copyright (C) 2024 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 "platform/MSWindowsTouchInjector.h" + +#include "base/Log.h" + +// Required for InjectTouchInput / InitializeTouchInjection +#include + +// Thread-local state for SetWinEventHook callback +HWND MSWindowsTouchInjector::s_foregroundTarget = NULL; +bool MSWindowsTouchInjector::s_foregroundArrived = false; + +MSWindowsTouchInjector::MSWindowsTouchInjector() +{ +} + +MSWindowsTouchInjector::~MSWindowsTouchInjector() +{ +} + +void MSWindowsTouchInjector::injectClickAt(SInt32 x, SInt32 y) +{ + LOG((CLOG_DEBUG "touch injector: click at %d,%d", x, y)); + + // Skip window activation — deskEnter() already restored the foreground window. + // The previous activateWindowAt + waitForForeground(300ms) was racing with + // deskEnter's SetForegroundWindow and caused unreliable injection (1 in 8). + + // Try touch injection first (produces WM_POINTERDOWN/WM_TOUCH), + // fall back to mouse click (produces WM_LBUTTONDOWN — always reliable). + if (!injectTouch(x, y)) { + LOG((CLOG_DEBUG "touch injector: touch failed, falling back to mouse click")); + injectMouseClick(x, y); + } +} + +HWND MSWindowsTouchInjector::activateWindowAt(SInt32 x, SInt32 y) +{ + POINT pt = {x, y}; + HWND child = WindowFromPoint(pt); + if (child == NULL) { + LOG((CLOG_DEBUG "touch injector: WindowFromPoint returned NULL for %d,%d", x, y)); + return NULL; + } + + // find the top-level (root) window + HWND root = GetAncestor(child, GA_ROOT); + if (root == NULL) { + root = child; + } + + LOG((CLOG_DEBUG "touch injector: activating window 0x%08x (root of 0x%08x) at %d,%d", root, child, x, y)); + + // try to set foreground directly first + DWORD currentThread = GetCurrentThreadId(); + DWORD targetThread = GetWindowThreadProcessId(root, NULL); + + AttachThreadInput(currentThread, targetThread, TRUE); + BOOL setResult = SetForegroundWindow(root); + AttachThreadInput(currentThread, targetThread, FALSE); + + if (setResult && GetForegroundWindow() == root) { + LOG((CLOG_DEBUG "touch injector: SetForegroundWindow succeeded immediately")); + return root; + } + + // if direct activation failed, wait event-driven using SetWinEventHook + LOG((CLOG_DEBUG "touch injector: waiting for foreground activation via hook")); + if (waitForForeground(root, 300)) { + LOG((CLOG_DEBUG "touch injector: foreground activation confirmed via hook")); + } else { + LOG((CLOG_DEBUG "touch injector: foreground wait timed out, proceeding anyway")); + } + + return root; +} + +bool MSWindowsTouchInjector::waitForForeground(HWND target, DWORD timeoutMs) +{ + // set up thread-local callback state + s_foregroundTarget = target; + s_foregroundArrived = false; + + // check if already foreground + if (GetForegroundWindow() == target) { + return true; + } + + // install an in-process hook for EVENT_SYSTEM_FOREGROUND + HWINEVENTHOOK hook = SetWinEventHook( + EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, NULL, foregroundEventProc, 0, 0, + WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS + ); + + if (hook == NULL) { + LOG((CLOG_WARN "touch injector: SetWinEventHook failed: %d", GetLastError())); + return false; + } + + // pump messages while waiting — keeps desk thread responsive + DWORD start = GetTickCount(); + while (!s_foregroundArrived) { + DWORD elapsed = GetTickCount() - start; + if (elapsed >= timeoutMs) { + break; + } + + DWORD remaining = timeoutMs - elapsed; + DWORD result = MsgWaitForMultipleObjects(0, NULL, FALSE, remaining, QS_ALLEVENTS); + if (result == WAIT_OBJECT_0) { + // messages available — process them so the hook callback fires + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + // WAIT_TIMEOUT or other — check loop condition + } + + UnhookWinEvent(hook); + + return s_foregroundArrived; +} + +void CALLBACK MSWindowsTouchInjector::foregroundEventProc( + HWINEVENTHOOK /*hook*/, DWORD event, HWND hwnd, LONG idObject, LONG /*idChild*/, DWORD /*eventThread*/, + DWORD /*eventTime*/ +) +{ + if (event == EVENT_SYSTEM_FOREGROUND && idObject == OBJID_WINDOW) { + if (hwnd == s_foregroundTarget || s_foregroundTarget == NULL) { + s_foregroundArrived = true; + } + } +} + +void MSWindowsTouchInjector::ensureTouchInitialized() +{ + if (m_touchInitialized) { + return; + } + + m_touchInitialized = true; + + // InitializeTouchInjection requires Windows 8+ + if (InitializeTouchInjection(1, TOUCH_FEEDBACK_DEFAULT)) { + m_touchAvailable = true; + LOG((CLOG_DEBUG "touch injector: InitializeTouchInjection succeeded")); + } else { + m_touchAvailable = false; + LOG((CLOG_WARN "touch injector: InitializeTouchInjection failed: %d", GetLastError())); + } +} + +bool MSWindowsTouchInjector::injectTouch(SInt32 x, SInt32 y) +{ + ensureTouchInitialized(); + + if (!m_touchAvailable) { + return false; + } + + POINTER_TOUCH_INFO contact = {}; + 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 = 512; + + // contact area — a small rectangle around the point + contact.rcContact.left = x - 2; + contact.rcContact.right = x + 2; + contact.rcContact.top = y - 2; + contact.rcContact.bottom = y + 2; + + // inject touch down + contact.pointerInfo.pointerFlags = POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT; + if (!InjectTouchInput(1, &contact)) { + LOG((CLOG_WARN "touch injector: InjectTouchInput DOWN failed: %d", GetLastError())); + return false; + } + + LOG((CLOG_DEBUG "touch injector: touch DOWN at %d,%d", x, y)); + + // Windows requires a short delay between touch down and up + Sleep(20); + + // inject touch up + contact.pointerInfo.pointerFlags = POINTER_FLAG_UP; + if (!InjectTouchInput(1, &contact)) { + LOG((CLOG_WARN "touch injector: InjectTouchInput UP failed: %d", GetLastError())); + // down succeeded but up failed — not much we can do + return true; + } + + LOG((CLOG_DEBUG "touch injector: touch UP at %d,%d", x, y)); + return true; +} + +void MSWindowsTouchInjector::injectMouseClick(SInt32 x, SInt32 y) +{ + // move cursor to position + SInt32 w = GetSystemMetrics(SM_CXSCREEN); + SInt32 h = GetSystemMetrics(SM_CYSCREEN); + DWORD nx = static_cast((65535.0f * x) / (w - 1) + 0.5f); + DWORD ny = static_cast((65535.0f * y) / (h - 1) + 0.5f); + + mouse_event(MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE, nx, ny, 0, 0); + mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0); + + Sleep(20); + + mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0); + + LOG((CLOG_DEBUG "touch injector: mouse click at %d,%d", x, y)); +} diff --git a/src/lib/platform/MSWindowsTouchInjector.h b/src/lib/platform/MSWindowsTouchInjector.h new file mode 100644 index 000000000..21bee7315 --- /dev/null +++ b/src/lib/platform/MSWindowsTouchInjector.h @@ -0,0 +1,72 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * Copyright (C) 2024 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 "base/EventTypes.h" + +#define WIN32_LEAN_AND_MEAN +#include + +/// Handles touch click injection for the touch-to-switch feature. +/// +/// Responsible for: +/// - Activating the window at a given screen coordinate +/// - Injecting a touch (or mouse) click at that coordinate +/// +/// All methods must be called from the desk thread. +class MSWindowsTouchInjector +{ +public: + MSWindowsTouchInjector(); + ~MSWindowsTouchInjector(); + + /// Activate the window at (x, y) and inject a click there. + /// Uses InjectTouchInput if available, falls back to mouse_event. + /// Must be called from the desk thread. + void injectClickAt(SInt32 x, SInt32 y); + +private: + /// Try to bring the window at (x, y) to the foreground. + /// Uses SetWinEventHook to wait for activation event-driven. + /// Returns the HWND that was activated, or NULL. + HWND activateWindowAt(SInt32 x, SInt32 y); + + /// Wait for a window to become foreground using SetWinEventHook. + /// Returns true if the target window became foreground within the timeout. + bool waitForForeground(HWND target, DWORD timeoutMs); + + /// Inject a touch down+up at (x, y) using InjectTouchInput. + /// Returns true if successful. + bool injectTouch(SInt32 x, SInt32 y); + + /// Inject a click at (x, y) using mouse_event as fallback. + void injectMouseClick(SInt32 x, SInt32 y); + + /// Initialize the touch injection API (lazy, called once). + void ensureTouchInitialized(); + + bool m_touchInitialized = false; + bool m_touchAvailable = false; + + /// SetWinEventHook callback context (thread-local by design). + static void CALLBACK foregroundEventProc( + HWINEVENTHOOK hook, DWORD event, HWND hwnd, LONG idObject, LONG idChild, DWORD eventThread, DWORD eventTime + ); + static HWND s_foregroundTarget; + static bool s_foregroundArrived; +}; diff --git a/src/lib/platform/dfwhook.h b/src/lib/platform/dfwhook.h index 8822663ae..b8ad99271 100644 --- a/src/lib/platform/dfwhook.h +++ b/src/lib/platform/dfwhook.h @@ -46,9 +46,14 @@ #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) #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 + +// Touch signature used by Windows to tag touch-generated mouse events +#define TOUCH_SIGNATURE_MASK 0xFFFFFF00 +#define TOUCH_SIGNATURE 0xFF515700 #define DESKFLOW_HOOK_FAKE_INPUT_VIRTUAL_KEY VK_CANCEL #define DESKFLOW_HOOK_FAKE_INPUT_SCANCODE 0 diff --git a/src/lib/server/ClientProxy1_9.cpp b/src/lib/server/ClientProxy1_9.cpp new file mode 100644 index 000000000..27389a95c --- /dev/null +++ b/src/lib/server/ClientProxy1_9.cpp @@ -0,0 +1,59 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * 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 + * 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, kMsgCGrabInput, 4) == 0) { + return recvGrabInput(); + } + return ClientProxy1_8::parseMessage(code); +} + +bool ClientProxy1_9::recvGrabInput() +{ + SInt16 x, y; + if (!ProtocolUtil::readf(getStream(), kMsgCGrabInput + 4, &x, &y)) { + return false; + } + LOG((CLOG_DEBUG "recv grab input from \"%s\" at %d,%d", getName().c_str(), x, y)); + + m_events->addEvent(Event( + m_events->forClientProxy().grabInput(), + 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..6c4590210 --- /dev/null +++ b/src/lib/server/ClientProxy1_9.h @@ -0,0 +1,35 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * 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 + * 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" + +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 recvGrabInput(); + + 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/Config.cpp b/src/lib/server/Config.cpp index aeeef31b4..14895209f 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") { + addOption("", kOptionTouchActivateScreen, s.parseBoolean(value)); } else if (name == "clientAddress") { m_ClientAddress = value; } else { @@ -1268,6 +1270,9 @@ const char *Config::getOptionName(OptionID id) if (id == kOptionClipboardSharingSize) { return "clipboardSharingSize"; } + if (id == kOptionTouchActivateScreen) { + return "touchActivateScreen"; + } return NULL; } @@ -1277,7 +1282,7 @@ String Config::getOptionValue(OptionID id, OptionValue value) id == kOptionScreenSwitchNeedsShift || id == kOptionScreenSwitchNeedsControl || id == kOptionScreenSwitchNeedsAlt || id == kOptionXTestXineramaUnaware || id == kOptionRelativeMouseMoves || id == kOptionWin32KeepForeground || id == kOptionScreenPreserveFocus || id == kOptionClipboardSharing || - id == kOptionClipboardSharingSize) { + id == kOptionClipboardSharingSize || id == kOptionTouchActivateScreen) { return (value != 0) ? "true" : "false"; } if (id == kOptionModifierMapForShift || id == kOptionModifierMapForControl || id == kOptionModifierMapForAlt || diff --git a/src/lib/server/PrimaryClient.cpp b/src/lib/server/PrimaryClient.cpp index a7024daec..8588ef4eb 100644 --- a/src/lib/server/PrimaryClient.cpp +++ b/src/lib/server/PrimaryClient.cpp @@ -71,6 +71,16 @@ void PrimaryClient::fakeInputEnd() } } +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 dfab74c7f..eb3e5cc4f 100644 --- a/src/lib/server/PrimaryClient.h +++ b/src/lib/server/PrimaryClient.h @@ -83,6 +83,12 @@ class PrimaryClient : public BaseClientProxy */ void fakeInputEnd(); + //! Activate the window at the given coordinates + void activateWindowAt(SInt32 x, SInt32 y); + + //! Inject a touch click at the given coordinates + void fakeTouchClick(SInt32 x, SInt32 y); + //@} //! @name accessors //@{ diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index 43a5fbae1..da32755a5 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -176,6 +176,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 +229,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(); @@ -301,6 +306,12 @@ void Server::adoptClient(BaseClientProxy *client) new TMethodEventJob(this, &Server::handleClientDisconnected, client) ); + // watch for client grab input requests (touch-to-switch) + m_events->adoptHandler( + m_events->forClientProxy().grabInput(), client, + new TMethodEventJob(this, &Server::handleGrabInputEvent, client) + ); + // name must be in our configuration if (!m_config->isScreen(client->getName())) { LOG((CLOG_WARN "unrecognised client name \"%s\", check server config", client->getName().c_str())); @@ -776,6 +787,14 @@ bool Server::isSwitchOkay( return false; } + // block edge switch if a touch switch happened recently + double elapsedTouchCooldown = m_touchSwitchCooldown.getTime(); + if (elapsedTouchCooldown > 0.0 && elapsedTouchCooldown < kTouchSwitchCooldownTime) { + LOG((CLOG_DEBUG "edge switch blocked by touch cooldown (%.2fs remaining)", + kTouchSwitchCooldownTime - elapsedTouchCooldown)); + return false; + } + // should we switch or not? bool preventSwitch = false; bool allowSwitch = false; @@ -1337,6 +1356,62 @@ void Server::handleSwitchInDirectionEvent(const Event &event, void *) } } +void Server::handleTouchActivatedPrimaryEvent(const Event &event, void *) +{ + IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); + LOG((CLOG_DEBUG "touch activated primary at %d,%d", info->m_x, info->m_y)); + + if (m_active != m_primaryClient) { + 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; + 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); + + switchScreen(m_primaryClient, x, y, false); + + m_primaryClient->activateWindowAt(x, y); + m_primaryClient->fakeTouchClick(x, y); + + m_touchSwitchCooldown.reset(); + LOG((CLOG_DEBUG "touch switch cooldown started")); + } +} + +void Server::handleGrabInputEvent(const Event &event, void *vclient) +{ + IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); + BaseClientProxy *client = static_cast(vclient); + + LOG((CLOG_DEBUG "client \"%s\" requests grab at %d,%d", getName(client).c_str(), info->m_x, info->m_y)); + + if (client != m_active) { + m_active->setJumpCursorPos(m_x, m_y); + + 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); + + switchScreen(client, x, y, false); + + m_touchSwitchCooldown.reset(); + LOG((CLOG_DEBUG "touch switch cooldown started")); + } +} + void Server::handleKeyboardBroadcastEvent(const Event &event, void *) { KeyboardBroadcastInfo *info = (KeyboardBroadcastInfo *)event.getData(); @@ -2087,6 +2162,7 @@ void Server::removeActiveClient(BaseClientProxy *client) { if (removeClient(client)) { forceLeaveClient(client); + m_events->removeHandler(m_events->forClientProxy().grabInput(), client); m_events->removeHandler(m_events->forClientProxy().disconnected(), client); if (m_clients.size() == 1 && m_oldClients.empty()) { m_events->addEvent(Event(m_events->forServer().disconnected(), this)); diff --git a/src/lib/server/Server.h b/src/lib/server/Server.h index 352539cbc..ca1298026 100644 --- a/src/lib/server/Server.h +++ b/src/lib/server/Server.h @@ -354,6 +354,8 @@ class Server : public INode void handleLockCursorToScreenEvent(const Event &, void *); void handleFakeInputBeginEvent(const Event &, void *); void handleFakeInputEndEvent(const Event &, void *); + void handleTouchActivatedPrimaryEvent(const Event &, void *); + void handleGrabInputEvent(const Event &, void *); void handleFileChunkSendingEvent(const Event &, void *); void handleFileRecieveCompletedEvent(const Event &, void *); @@ -522,4 +524,8 @@ class Server : public INode ClientListener *m_clientListener; deskflow::ServerArgs m_args; + + // prevents edge-triggered switches from immediately undoing touch switches + Stopwatch m_touchSwitchCooldown; + static constexpr double kTouchSwitchCooldownTime = 0.5; };