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/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..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,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..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) + ClientProxyEvents() : m_ready(Event::kUnknown), m_disconnected(Event::kUnknown), m_grabInput(Event::kUnknown) { } @@ -410,11 +410,14 @@ class ClientProxyEvents : public EventTypes */ Event::Type disconnected(); + Event::Type grabInput(); + //@} private: Event::Type m_ready; Event::Type m_disconnected; + Event::Type m_grabInput; }; class ClientProxyUnknownEvents : public EventTypes @@ -603,7 +606,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 +654,8 @@ class IPrimaryScreenEvents : public EventTypes //! end of fake input event type Event::Type fakeInputEnd(); + Event::Type touchActivatedPrimary(); + //@} private: @@ -664,6 +670,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 +680,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 +716,8 @@ class IScreenEvents : public EventTypes */ Event::Type resume(); + Event::Type grabInput(); + //@} private: @@ -715,6 +725,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..d06f6ae0d 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 @@ -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().grabInput(), m_screen->getEventTarget(), + new TMethodEventJob(this, &Client::handleGrabInput) + ); 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().grabInput(), m_screen->getEventTarget()); cleanupTimer(); cleanupScreen(); @@ -719,6 +725,15 @@ 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_DEBUG1 "requesting grab input at %d,%d", 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..6389c015e 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,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..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,6 +372,12 @@ bool ServerProxy::onGrabClipboard(ClipboardID id) return true; } +void ServerProxy::grabInput(SInt32 x, SInt32 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) { String data = IClipboard::marshall(clipboard); diff --git a/src/lib/client/ServerProxy.h b/src/lib/client/ServerProxy.h index 743c3393f..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,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..cf816cca0 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,6 +200,9 @@ class IPlatformScreen : public IScreen, public IPrimaryScreen, public ISecondary virtual void pollPressedKeys(KeyButtonSet &pressedKeys) const = 0; 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; virtual void clearDraggingFilename() = 0; diff --git a/src/lib/deskflow/Screen.cpp b/src/lib/deskflow/Screen.cpp index f3581201c..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 @@ -488,6 +493,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..495776990 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 @@ -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 @@ -236,6 +238,8 @@ class Screen : public IScreen void setEnableDragDrop(bool enabled); + 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/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..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,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..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,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/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 c9afc4d31..fd5ef9297 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,13 @@ #include "platform/dfwhook.h" #include +#include + +#ifndef _NTDEF_ +typedef LONG NTSTATUS; +#endif +#include +#include // these are only defined when WINVER >= 0x0500 #if !defined(SPI_GETMOUSESPEED) @@ -86,6 +93,10 @@ #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 +#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) { @@ -316,6 +327,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)); @@ -409,25 +425,93 @@ 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); } +void setCursorVisibility(bool visible); + 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: + return PA_NOACTIVATE; + + case WM_POINTERDOWN: { + MSWindowsDesks *self = reinterpret_cast( + GetWindowLongPtr(hwnd, GWLP_USERDATA)); + if (self) { + UINT32 pointerId = GET_POINTERID_WPARAM(wParam); + DWORD pointerType = PT_POINTER; + 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_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; + } } break; } - if (hide && IsWindowVisible(hwnd)) { - ReleaseCapture(); - SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + 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); + 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 { + 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_MOUSEMOVE: { + LPARAM extraInfo = GetMessageExtraInfo(); + if ((extraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE) { + break; + } + + MSWindowsDesks *self = reinterpret_cast( + GetWindowLongPtr(hwnd, GWLP_USERDATA)); + if (self && IsWindowVisible(hwnd)) { + // 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); + } + break; + } } return DefWindowProc(hwnd, msg, wParam, lParam); @@ -446,6 +530,79 @@ void MSWindowsDesks::deskMouseMove(SInt32 x, SInt32 y) const ); } +void MSWindowsDesks::deskFakeTouchClick(SInt32 x, SInt32 y) const +{ + POINT pt = {x, y}; + HWND target = WindowFromPoint(pt); + HWND fg = GetForegroundWindow(); + + 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(); + + BOOL attached = FALSE; + if (targetThread != 0 && targetThread != curThread) { + attached = AttachThreadInput(targetThread, curThread, TRUE); + } + + mouse_event(MOUSEEVENTF_MOVE, 0, 0, 0, 0); + SetForegroundWindow(root); + BringWindowToTop(root); + + if (attached) { + AttachThreadInput(targetThread, curThread, FALSE); + } + } + } + + static bool touchInitialized = false; + if (!touchInitialized) { + touchInitialized = InitializeTouchInjection(2, TOUCH_FEEDBACK_NONE) != 0; + } + + if (touchInitialized) { + POINTER_TOUCH_INFO contact = {}; + contact.pointerInfo.pointerType = PT_TOUCH; + contact.pointerInfo.pointerId = 1; + 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; + + 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; + } + + 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); +} + void MSWindowsDesks::deskMouseRelativeMove(SInt32 dx, SInt32 dy) const { // relative moves are subject to cursor acceleration which we don't @@ -517,13 +674,33 @@ 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); + exStyle = (exStyle | WS_EX_TRANSPARENT) & ~WS_EX_LAYERED; + SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle); + } + + bool touchEnter = false; + 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); + } else { + 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 @@ -531,11 +708,21 @@ void MSWindowsDesks::deskEnter(Desk *desk) // (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 (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; } @@ -550,8 +737,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; @@ -567,6 +755,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); @@ -599,27 +791,185 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) AttachThreadInput(thatThread, thisThread, FALSE); } } + + registerTouchRawInput(desk->m_window, true); } 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); + 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); - // 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. - LOG_DEBUG1("centering cursor on leave: %+d,%+d", m_xCenter, m_yCenter); ARCH->sleep(0.03); deskMouseMove(m_xCenter, m_yCenter); + + registerTouchRawInput(desk->m_window, true); + } +} + +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; + } + + // 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; +} + +void MSWindowsDesks::registerTouchRawInput(HWND window, bool enable) +{ + RAWINPUTDEVICE rid = {}; + rid.usUsagePage = 0x0D; + rid.usUsage = 0x04; + rid.dwFlags = enable ? RIDEV_INPUTSINK : RIDEV_REMOVE; + rid.hwndTarget = enable ? window : NULL; + + RegisterRawInputDevices(&rid, 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; + } } } @@ -639,6 +989,7 @@ 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)); } catch (...) { // ignore @@ -660,6 +1011,54 @@ void MSWindowsDesks::deskThread(void *vdesk) DispatchMessage(&msg); continue; + case WM_INPUT: { + 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); + + 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); + 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_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 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")); + } + } + } + } + continue; + } + case DESKFLOW_MSG_SWITCH: if (!m_noHooks) { MSWindowsHook::uninstall(); @@ -719,6 +1118,57 @@ 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_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 eb225a381..b042c0f21 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 @@ -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); @@ -292,4 +311,12 @@ class MSWindowsDesks // true if program should stop on desk switch. bool m_stopOnDeskSwitch; + + std::unordered_map m_hidTouchDevices; + + bool m_pendingTouchUp = false; + bool m_touchLifted = false; + SInt32 m_pendingTouchX = 0; + SInt32 m_pendingTouchY = 0; + }; diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index b51b1a734..e80437835 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 @@ -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,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 +591,28 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) // decode the message MSLLHOOKSTRUCT *info = reinterpret_cast(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) { + 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; + } + } + } + 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..79e45149c 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 @@ -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" @@ -43,6 +44,7 @@ #include #include #include +#include // suppress warning about GetVersionEx, which is used indirectly in this // compilation unit. @@ -124,7 +126,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 +137,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 +155,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 +482,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 set to %s", m_touchActivateScreen ? "true" : "false")); + } + } } void MSWindowsScreen::setSequenceNumber(UInt32 seqNum) @@ -957,6 +971,34 @@ 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: + 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) { + LOG((CLOG_DEBUG "DESKFLOW_MSG_TOUCH: debounced (%.0fms elapsed)", + m_touchDebounceTimer.getTime() * 1000.0)); + 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 at %d,%d", x, y)); + sendEvent(m_events->forIPrimaryScreen().touchActivatedPrimary(), + MotionInfo::alloc(x, y)); + } else { + LOG((CLOG_INFO "hook: touch requesting grab input at %d,%d", x, y)); + sendEvent(m_events->forIScreen().grabInput(), + MotionInfo::alloc(x, y)); + } + } + return true; } if (m_isPrimary) { @@ -1043,6 +1085,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 +1459,53 @@ 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)) { + 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) { + 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; + pt.x = GET_X_LPARAM(lParam); + pt.y = GET_Y_LPARAM(lParam); + + if (m_isPrimary) { + 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 grab input at %d,%d", pt.x, pt.y)); + sendEvent(m_events->forIScreen().grabInput(), + MotionInfo::alloc(pt.x, pt.y)); + } + + return true; +} + bool MSWindowsScreen::onDisplayChange() { // screen resolution may have changed. save old shape. @@ -1877,6 +1975,55 @@ String MSWindowsScreen::getSecureInputApp() const return ""; } +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; + } + + 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); + } + DWORD curThread = GetCurrentThreadId(); + BOOL attached = FALSE; + if (foreThread != 0 && foreThread != curThread) { + attached = AttachThreadInput(foreThread, curThread, TRUE); + } + BOOL ok = SetForegroundWindow(root); + if (attached) { + AttachThreadInput(foreThread, curThread, FALSE); + } + + 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)); + } +} + +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 8688d4286..6aa31816f 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 @@ -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,8 @@ 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; protected: // IPlatformScreen overrides @@ -190,6 +193,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 +362,9 @@ class MSWindowsScreen : public PlatformScreen PrimaryKeyDownList m_primaryKeyDownList; MSWindowsPowerManager m_powerManager; + + bool m_touchActivateScreen; + + Stopwatch m_touchDebounceTimer; + static constexpr double kTouchDebounceTime = 0.15; }; diff --git a/src/lib/platform/dfwhook.h b/src/lib/platform/dfwhook.h index 8822663ae..f3c8ac41f 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 @@ -46,13 +38,19 @@ #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 +// 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" { diff --git a/src/lib/server/ClientProxy1_9.cpp b/src/lib/server/ClientProxy1_9.cpp new file mode 100644 index 000000000..6adb48ac2 --- /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 "received client \"%s\" grab input request 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..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/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..7f7b88bcc 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,6 +83,9 @@ 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 43a5fbae1..e52ff4e68 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 @@ -40,6 +40,7 @@ #include "server/ClientProxyUnknown.h" #include "server/PrimaryClient.h" +#include #include #include #include @@ -176,6 +177,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 +230,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(); @@ -479,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; @@ -776,6 +786,13 @@ bool Server::isSwitchOkay( return false; } + double elapsedTouchCooldown = m_touchSwitchCooldown.getTime(); + if (elapsedTouchCooldown > 0.0 && elapsedTouchCooldown < kTouchSwitchCooldownTime) { + LOG((CLOG_DEBUG1 "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 +1354,69 @@ 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)); + + 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); + + // 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_DEBUG1 "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_DEBUG1 "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_DEBUG1 "touch switch cooldown started")); + } +} + void Server::handleKeyboardBroadcastEvent(const Event &event, void *) { KeyboardBroadcastInfo *info = (KeyboardBroadcastInfo *)event.getData(); @@ -1989,6 +2069,10 @@ bool Server::addClient(BaseClientProxy *client) m_events->forClipboard().clipboardChanged(), client->getEventTarget(), new TMethodEventJob(this, &Server::handleClipboardChanged, client) ); + m_events->adoptHandler( + m_events->forClientProxy().grabInput(), client->getEventTarget(), + new TMethodEventJob(this, &Server::handleGrabInputEvent, client) + ); // add to list m_clientSet.insert(client); @@ -2017,6 +2101,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().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 352539cbc..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 @@ -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 handleGrabInputEvent(const Event &, void *); void handleKeyboardBroadcastEvent(const Event &, void *); void handleLockCursorToScreenEvent(const Event &, void *); void handleFakeInputBeginEvent(const Event &, void *); @@ -482,6 +484,10 @@ class Server : public INode bool m_switchTwoTapArmed; SInt32 m_switchTwoTapZone; + // prevents edge-triggered switches from immediately undoing touch switches + Stopwatch m_touchSwitchCooldown; + static constexpr double kTouchSwitchCooldownTime = 0.5; + // modifiers needed before switching bool m_switchNeedsShift; bool m_switchNeedsControl;