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 88568ff96..e925cef07 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(); @@ -240,6 +246,15 @@ void Client::enter(SInt32 xAbs, SInt32 yAbs, UInt32, KeyModifierMask mask, bool) m_screen->mouseMove(xAbs, yAbs); m_screen->enter(mask); + if (m_pendingTouchActivation) { + m_pendingTouchActivation = false; + LOG((CLOG_DEBUG1 "touch: replaying click at %d,%d (cursor at %d,%d)", + m_touchActivateX, m_touchActivateY, xAbs, yAbs)); + m_screen->activateWindowAt(m_touchActivateX, m_touchActivateY); + m_screen->mouseDown(kButtonLeft); + m_screen->mouseUp(kButtonLeft); + } + if (m_sendFileThread) { StreamChunker::interruptFile(); m_sendFileThread.reset(nullptr); @@ -737,6 +752,18 @@ 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_pendingTouchActivation = true; + m_touchActivateX = info->m_x; + m_touchActivateY = 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 7460b0bb1..c7f9b0992 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 @@ -221,6 +221,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 *); @@ -260,4 +261,7 @@ class Client : public IClient, public INode size_t m_maximumClipboardSize; deskflow::ClientArgs m_args; size_t m_resolvedAddressesCount = 0; + bool m_pendingTouchActivation = false; + SInt32 m_touchActivateX = 0; + SInt32 m_touchActivateY = 0; }; diff --git a/src/lib/client/ServerProxy.cpp b/src/lib/client/ServerProxy.cpp index 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..b5d9c2407 100644 --- a/src/lib/deskflow/IPlatformScreen.h +++ b/src/lib/deskflow/IPlatformScreen.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -200,6 +200,8 @@ 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 */ } + // Drag-and-drop overrides virtual String &getDraggingFilename() = 0; virtual void clearDraggingFilename() = 0; diff --git a/src/lib/deskflow/Screen.cpp b/src/lib/deskflow/Screen.cpp index f3581201c..8af861e84 100644 --- a/src/lib/deskflow/Screen.cpp +++ b/src/lib/deskflow/Screen.cpp @@ -488,6 +488,11 @@ void Screen::leaveSecondary() m_screen->fakeAllKeysUp(); } +void Screen::activateWindowAt(SInt32 x, SInt32 y) +{ + m_screen->activateWindowAt(x, y); +} + String Screen::getSecureInputApp() const { return m_screen->getSecureInputApp(); diff --git a/src/lib/deskflow/Screen.h b/src/lib/deskflow/Screen.h index 9e4c3ef63..628ba87bc 100644 --- a/src/lib/deskflow/Screen.h +++ b/src/lib/deskflow/Screen.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -236,6 +236,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/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index c9afc4d31..a65e63d9a 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2004 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -32,6 +32,7 @@ #include "platform/dfwhook.h" #include +#include // these are only defined when WINVER >= 0x0500 #if !defined(SPI_GETMOUSESPEED) @@ -123,6 +124,7 @@ MSWindowsDesks::MSWindowsDesks( : m_isPrimary(isPrimary), m_noHooks(noHooks), m_isOnScreen(m_isPrimary), + m_deskLeaveTime(0), m_x(0), m_y(0), m_w(0), @@ -409,25 +411,60 @@ void MSWindowsDesks::destroyWindow(HWND hwnd) const LRESULT CALLBACK MSWindowsDesks::primaryDeskProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + switch (msg) { + case WM_SETCURSOR: + SetCursor(NULL); + return TRUE; + } return DefWindowProc(hwnd, msg, wParam, lParam); } LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { - // 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_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_DEBUG1 "secondary WM_POINTERDOWN touch at %d,%d", x, y)); + PostThreadMessage(self->m_threadID, DESKFLOW_MSG_TOUCH, + static_cast(x), static_cast(y)); + return 0; + } } break; } - if (hide && IsWindowVisible(hwnd)) { - ReleaseCapture(); - SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + case WM_MOUSEMOVE: { + LPARAM extraInfo = GetMessageExtraInfo(); + if ((extraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE) { + LOG((CLOG_DEBUG "secondary WM_MOUSEMOVE: touch signature detected, keeping hider")); + break; + } + + MSWindowsDesks *self = reinterpret_cast( + GetWindowLongPtr(hwnd, GWLP_USERDATA)); + if (self && IsWindowVisible(hwnd)) { + if (GetTickCount64() - self->m_deskLeaveTime < 200) { + break; + } + ReleaseCapture(); + SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + HCURSOR arrow = LoadCursor(NULL, IDC_ARROW); + SetClassLongPtr(hwnd, GCLP_HCURSOR, reinterpret_cast(arrow)); + SetCursor(arrow); + } + break; + } } return DefWindowProc(hwnd, msg, wParam, lParam); @@ -519,11 +556,19 @@ void MSWindowsDesks::deskEnter(Desk *desk) { if (!m_isPrimary) { ReleaseCapture(); + + LONG_PTR exStyle = GetWindowLongPtr(desk->m_window, GWL_EXSTYLE); + // let hit-testing pass through to windows underneath + SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle | WS_EX_TRANSPARENT); } + 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 @@ -550,8 +595,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 +613,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); @@ -600,25 +650,21 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) } } } else { - // move hider window under the cursor center, raise, and show it - SetWindowPos(desk->m_window, HWND_TOP, m_xCenter, m_yCenter, 1, 1, SWP_NOACTIVATE | SWP_SHOWWINDOW); + 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); + SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle & ~WS_EX_TRANSPARENT); + + SetWindowPos(desk->m_window, HWND_TOPMOST, m_x, m_y, m_w, m_h, SWP_NOACTIVATE | SWP_SHOWWINDOW); - // watch for mouse motion. if we see any then we hide the - // hider window so the user can use the physically attached - // mouse if desired. we'd rather not capture the mouse but - // we aren't notified when the mouse leaves our window. SetCapture(desk->m_window); - // windows can take a while to hide the cursor, so wait a few milliseconds to ensure the cursor - // is hidden before centering. this doesn't seem to affect the fluidity of the transition. - // without this, the cursor appears to flicker in the center of the screen which is annoying. - // a slightly more elegant but complex solution could be to use a timed event. - // 30 ms seems to work well enough without making the transition feel janky; a lower number - // would be better but 10 ms doesn't seem to be quite long enough, as we get noticeable flicker. - // this is largely a balance and out of our control, since windows can be unpredictable... - // maybe another approach would be to repeatedly check the cursor visibility until it is hidden. - LOG_DEBUG1("centering cursor on leave: %+d,%+d", m_xCenter, m_yCenter); + // brief delay for the hider window's blank cursor to take effect ARCH->sleep(0.03); + m_deskLeaveTime = GetTickCount64(); deskMouseMove(m_xCenter, m_yCenter); } } @@ -639,7 +685,54 @@ 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)); + + RAWINPUTDEVICE rids[4] = {}; + rids[0].usUsagePage = 0x0D; rids[0].usUsage = 0x04; // Touch Screen + rids[0].dwFlags = RIDEV_INPUTSINK; rids[0].hwndTarget = desk->m_window; + rids[1].usUsagePage = 0x0D; rids[1].usUsage = 0x05; // Touch Pad + rids[1].dwFlags = RIDEV_INPUTSINK; rids[1].hwndTarget = desk->m_window; + rids[2].usUsagePage = 0x0D; rids[2].usUsage = 0x01; // Digitizer + rids[2].dwFlags = RIDEV_INPUTSINK; rids[2].hwndTarget = desk->m_window; + rids[3].usUsagePage = 0x0D; rids[3].usUsage = 0x02; // Pen + rids[3].dwFlags = RIDEV_INPUTSINK; rids[3].hwndTarget = desk->m_window; + if (RegisterRawInputDevices(rids, 4, sizeof(RAWINPUTDEVICE))) { + LOG((CLOG_DEBUG "desk %s: registered raw touch input on desk window", + desk->m_name.c_str())); + } else { + if (RegisterRawInputDevices(rids, 1, sizeof(RAWINPUTDEVICE))) { + LOG((CLOG_DEBUG "desk %s: registered touch screen raw input", + desk->m_name.c_str())); + } else { + LOG((CLOG_WARN "desk %s: failed to register raw touch input, error=%d", + desk->m_name.c_str(), GetLastError())); + } + } + + // enumerate connected HID devices so we can identify touch hardware in logs + 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 "desk %s: %u raw input device(s) connected", + desk->m_name.c_str(), 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; + } } catch (...) { // ignore LOG((CLOG_DEBUG "can't create desk window for %s", desk->m_name.c_str())); @@ -660,6 +753,39 @@ 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 && m_isPrimary && + raw->data.hid.dwCount > 0 && raw->data.hid.dwSizeHid > 0) { + POINT pt; + GetCursorPos(&pt); + LOG((CLOG_DEBUG1 "desk raw touch at %d,%d", pt.x, pt.y)); + PostThreadMessage(m_threadID, DESKFLOW_MSG_TOUCH, + static_cast(pt.x), static_cast(pt.y)); + } else if (raw->header.dwType == RIM_TYPEHID && !m_isPrimary) { + LOG((CLOG_DEBUG "WM_INPUT: HID touch skipped (not primary)")); + } + } + } + continue; + } + case DESKFLOW_MSG_SWITCH: if (!m_noHooks) { MSWindowsHook::uninstall(); diff --git a/src/lib/platform/MSWindowsDesks.h b/src/lib/platform/MSWindowsDesks.h index eb225a381..76dd54822 100644 --- a/src/lib/platform/MSWindowsDesks.h +++ b/src/lib/platform/MSWindowsDesks.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2004 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -252,6 +252,9 @@ class MSWindowsDesks // true if mouse has entered the screen bool m_isOnScreen; + // suppress WM_MOUSEMOVE hider dismissal briefly after deskLeave + ULONGLONG m_deskLeaveTime; + // our resources ATOM m_deskClass; HCURSOR m_cursor; diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index b51b1a734..8c791ffd0 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,30 @@ 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) { + 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); + // Only eat in relay mode (cursor has left this screen) to + // prevent edge detection and isLockedToScreen from racing. + // In watch mode (cursor on server), let it through for + // normal touch behavior on the server's own screens. + if (g_isPrimary && g_mode == kHOOK_RELAY_EVENTS) { + 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..0b24f1d9c 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,28 @@ 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 || m_isOnScreen) + return true; + { + if (m_touchDebounceTimer.getTime() < kTouchDebounceTime) + return true; + m_touchDebounceTimer.reset(); + + SInt32 x = static_cast(wParam); + SInt32 y = static_cast(lParam); + if (m_isPrimary) { + LOG((CLOG_INFO "hook: touch activating primary 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 +1079,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 +1453,54 @@ 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 +1970,50 @@ 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)); + } +} + 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..91fcb0739 100644 --- a/src/lib/platform/MSWindowsScreen.h +++ b/src/lib/platform/MSWindowsScreen.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -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,7 @@ class MSWindowsScreen : public PlatformScreen virtual String &getDraggingFilename(); virtual const String &getDropTarget() const; String getSecureInputApp() const override; + void activateWindowAt(SInt32 x, SInt32 y) override; protected: // IPlatformScreen overrides @@ -190,6 +192,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 +361,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..070d4cbbf 100644 --- a/src/lib/server/PrimaryClient.cpp +++ b/src/lib/server/PrimaryClient.cpp @@ -71,6 +71,11 @@ void PrimaryClient::fakeInputEnd() } } +void PrimaryClient::activateWindowAt(SInt32 x, SInt32 y) +{ + m_screen->activateWindowAt(x, y); +} + SInt32 PrimaryClient::getJumpZoneSize() const { return m_screen->getJumpZoneSize(); diff --git a/src/lib/server/PrimaryClient.h b/src/lib/server/PrimaryClient.h index dfab74c7f..8203eab15 100644 --- a/src/lib/server/PrimaryClient.h +++ b/src/lib/server/PrimaryClient.h @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -83,6 +83,8 @@ class PrimaryClient : public BaseClientProxy */ void fakeInputEnd(); + void activateWindowAt(SInt32 x, SInt32 y); + //@} //! @name accessors //@{ diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index 43a5fbae1..057054d2c 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * Copyright (C) 2012 Symless Ltd. + * Copyright (C) 2012-2026 Symless Ltd. * Copyright (C) 2002 Chris Schoeneman * * This package is free software; you can redistribute it and/or @@ -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(); @@ -776,6 +782,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 +1350,63 @@ 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)); + + 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); + + // The hook eats the original touch event, so the window under the + // touch point never receives it. Explicitly activate that window. + m_primaryClient->activateWindowAt(x, y); + + m_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 +2059,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 +2091,7 @@ bool Server::removeClient(BaseClientProxy *client) m_events->removeHandler(m_events->forIScreen().shapeChanged(), client->getEventTarget()); m_events->removeHandler(m_events->forClipboard().clipboardGrabbed(), client->getEventTarget()); m_events->removeHandler(m_events->forClipboard().clipboardChanged(), client->getEventTarget()); + m_events->removeHandler(m_events->forClientProxy().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;