From 8380520cd40a125707b5414b299d99a99b67dad9 Mon Sep 17 00:00:00 2001 From: Stefan Verleysen Date: Wed, 11 Feb 2026 10:53:09 -0500 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20touch=20activates=20screen=20?= =?UTF-8?q?=E2=80=94=20switch=20to=20touched=20computer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a touchActivateScreen option that switches keyboard/mouse focus to whichever computer the user touches. Works bidirectionally between server and clients, and detects touch in all applications including Chrome, Electron, UWP, and legacy Win32 apps. Touch detection uses three independent paths for broad hardware coverage: low-level mouse hook (dwExtraInfo MI_WP_SIGNATURE), raw input (RIDEV_INPUTSINK on desk thread for WM_INPUT), and WM_POINTER messages (Win8+ API). All three converge through debounced event dispatch to the server's screen-switch logic. Raw input is registered on the desk thread window rather than the main window because the main event loop uses QS_ALLPOSTMESSAGE, which never wakes for WM_INPUT messages. Cursor hiding on touchscreen clients uses a full-screen hider window with a blank cursor class instead of ShowCursor(FALSE), which is unreliable on touch hardware. WS_EX_TRANSPARENT is toggled on leave/enter for correct hit-testing. The server synthesizes a click and forces the foreground window after touch-triggered switches so the window under the touch point receives focus. A 500ms cooldown prevents edge-triggered switches from immediately undoing touch switches. Tested on: - Server: ASUS 3090DEV, i9-12900K, RTX 3090, Windows 11 Enterprise Build 26100 (64-bit), USB HID touch screen (VID 0457 / PID 0819) - Client: Microsoft Surface Book (1st gen), integrated touchscreen, Windows 11 - Applications tested: Chrome, Cursor (Electron), Windows Settings (UWP), Notepad, Synergy GUI, Start menu, File Explorer --- src/gui/CMakeLists.txt | 2 + src/gui/src/ServerConfig.cpp | 10 +- src/gui/src/ServerConfig.h | 9 ++ src/gui/src/ServerConfigDialog.cpp | 9 ++ src/gui/src/ServerConfigDialogBase.ui | 11 ++ src/lib/base/EventTypes.cpp | 3 + src/lib/base/EventTypes.h | 34 ++++++- src/lib/client/Client.cpp | 16 +++ src/lib/client/Client.h | 1 + src/lib/client/ServerProxy.cpp | 6 ++ src/lib/client/ServerProxy.h | 7 ++ src/lib/deskflow/option_types.h | 1 + src/lib/deskflow/protocol_types.cpp | 1 + src/lib/deskflow/protocol_types.h | 5 + src/lib/platform/MSWindowsDesks.cpp | 101 ++++++++++++++----- src/lib/platform/MSWindowsHook.cpp | 36 +++++++ src/lib/platform/MSWindowsHook.h | 4 + src/lib/platform/MSWindowsScreen.cpp | 129 +++++++++++++++++++++++- src/lib/platform/MSWindowsScreen.h | 10 ++ src/lib/platform/dfwhook.h | 3 +- src/lib/server/ClientProxy1_0.cpp | 22 ++++ src/lib/server/ClientProxy1_0.h | 1 + src/lib/server/Config.cpp | 2 + src/lib/server/Server.cpp | 139 ++++++++++++++++++++++++++ src/lib/server/Server.h | 7 ++ 25 files changed, 535 insertions(+), 34 deletions(-) diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 0064323e2..b042bcad4 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -41,7 +41,9 @@ include_directories(./src) # gui library autogen headers: # qt doesn't seem to auto include the autogen headers for libraries. +# single-config generators (Make) use include/, multi-config (VS) use include_$/ include_directories(${PROJECT_BINARY_DIR}/src/lib/gui/gui_autogen/include) +include_directories(${PROJECT_BINARY_DIR}/src/lib/gui/gui_autogen/include_$) # generated includes include_directories(${PROJECT_BINARY_DIR}/config) diff --git a/src/gui/src/ServerConfig.cpp b/src/gui/src/ServerConfig.cpp index 39d2aa8e6..018ea70a4 100644 --- a/src/gui/src/ServerConfig.cpp +++ b/src/gui/src/ServerConfig.cpp @@ -80,7 +80,8 @@ bool ServerConfig::operator==(const ServerConfig &sc) const m_SwitchCornerSize == sc.m_SwitchCornerSize && m_SwitchCorners == sc.m_SwitchCorners && m_Hotkeys == sc.m_Hotkeys && m_pAppConfig == sc.m_pAppConfig && m_DisableLockToScreen == sc.m_DisableLockToScreen && m_ClipboardSharing == sc.m_ClipboardSharing && - m_ClipboardSharingSize == sc.m_ClipboardSharingSize && m_pMainWindow == sc.m_pMainWindow; + m_ClipboardSharingSize == sc.m_ClipboardSharingSize && m_TouchActivateScreen == sc.m_TouchActivateScreen && + m_pMainWindow == sc.m_pMainWindow; } void ServerConfig::save(QFile &file) const @@ -127,6 +128,7 @@ void ServerConfig::commit() settings().setValue("disableLockToScreen", disableLockToScreen()); settings().setValue("clipboardSharing", clipboardSharing()); settings().setValue("clipboardSharingSize", QVariant::fromValue(clipboardSharingSize())); + settings().setValue("touchActivateScreen", touchActivateScreen()); if (!getClientAddress().isEmpty()) { settings().setValue("clientAddress", getClientAddress()); @@ -182,6 +184,9 @@ void ServerConfig::recall() settings().value("clipboardSharingSize", (int)ServerConfig::defaultClipboardSharingSize()).toULongLong() ); setClipboardSharing(settings().value("clipboardSharing", true).toBool()); + setTouchActivateScreen( + settings().value("touchActivateScreen", + settings().value("touchInputLocal", false)).toBool()); setClientAddress(settings().value("clientAddress", "").toString()); readSettings(settings(), switchCorners(), "switchCorner", 0, static_cast(NumSwitchCorners)); @@ -310,6 +315,9 @@ QTextStream &operator<<(QTextStream &outStream, const ServerConfig &config) outStream << "\t" << "switchCornerSize = " << config.switchCornerSize() << Qt::endl; + outStream << "\t" + << "touchActivateScreen = " << (config.touchActivateScreen() ? "true" : "false") << Qt::endl; + foreach (const Hotkey &hotkey, config.hotkeys()) outStream << hotkey; diff --git a/src/gui/src/ServerConfig.h b/src/gui/src/ServerConfig.h index 2c99fa7fd..fc489e814 100644 --- a/src/gui/src/ServerConfig.h +++ b/src/gui/src/ServerConfig.h @@ -128,6 +128,10 @@ class ServerConfig : public ScreenConfig, public deskflow::gui::IServerConfig { return m_ClipboardSharingSize; } + bool touchActivateScreen() const + { + return m_TouchActivateScreen; + } static size_t defaultClipboardSharingSize(); // @@ -224,6 +228,10 @@ class ServerConfig : public ScreenConfig, public deskflow::gui::IServerConfig { m_ClipboardSharing = on; } + void setTouchActivateScreen(bool on) + { + m_TouchActivateScreen = on; + } void setConfigFile(const QString &configFile); void setUseExternalConfig(bool useExternalConfig); size_t setClipboardSharingSize(size_t size); @@ -253,6 +261,7 @@ class ServerConfig : public ScreenConfig, public deskflow::gui::IServerConfig int m_SwitchCornerSize = 0; bool m_DisableLockToScreen = false; bool m_ClipboardSharing = true; + bool m_TouchActivateScreen = false; QString m_ClientAddress = ""; QList m_SwitchCorners; HotkeyList m_Hotkeys; diff --git a/src/gui/src/ServerConfigDialog.cpp b/src/gui/src/ServerConfigDialog.cpp index e557d1788..9d5ad9da2 100644 --- a/src/gui/src/ServerConfigDialog.cpp +++ b/src/gui/src/ServerConfigDialog.cpp @@ -68,6 +68,7 @@ ServerConfigDialog::ServerConfigDialog(QWidget *parent, ServerConfig &config, Ap m_pCheckBoxCornerBottomRight->setChecked(serverConfig().switchCorner(static_cast(BottomRight))); m_pSpinBoxSwitchCornerSize->setValue(serverConfig().switchCornerSize()); m_pCheckBoxDisableLockToScreen->setChecked(serverConfig().disableLockToScreen()); + m_pCheckBoxTouchActivateScreen->setChecked(serverConfig().touchActivateScreen()); m_pCheckBoxEnableClipboard->setChecked(serverConfig().clipboardSharing()); int clipboardSharingSizeM = static_cast(serverConfig().clipboardSharingSize() / 1024); @@ -142,6 +143,10 @@ ServerConfigDialog::ServerConfigDialog(QWidget *parent, ServerConfig &config, Ap serverConfig().setDisableLockToScreen(v); onChange(); }); + connect(m_pCheckBoxTouchActivateScreen, &QCheckBox::stateChanged, this, [this](const int &v) { + serverConfig().setTouchActivateScreen(v); + onChange(); + }); connect(m_pCheckBoxCornerTopLeft, &QCheckBox::stateChanged, this, [this](const int &v) { serverConfig().setSwitchCorner(static_cast(TopLeft), v); onChange(); @@ -192,6 +197,10 @@ ServerConfigDialog::ServerConfigDialog(QWidget *parent, ServerConfig &config, Ap serverConfig().setDisableLockToScreen(v == Qt::Checked); onChange(); }); + connect(m_pCheckBoxTouchActivateScreen, &QCheckBox::checkStateChanged, this, [this](const Qt::CheckState &v) { + serverConfig().setTouchActivateScreen(v == Qt::Checked); + onChange(); + }); connect(m_pCheckBoxCornerTopLeft, &QCheckBox::checkStateChanged, this, [this](const Qt::CheckState &v) { serverConfig().setSwitchCorner(static_cast(TopLeft), v == Qt::Checked); onChange(); diff --git a/src/gui/src/ServerConfigDialogBase.ui b/src/gui/src/ServerConfigDialogBase.ui index a8a420821..98034b788 100644 --- a/src/gui/src/ServerConfigDialogBase.ui +++ b/src/gui/src/ServerConfigDialogBase.ui @@ -798,6 +798,16 @@ + + + + Switch screens on touch + + + Touch any screen to switch to that computer + + + @@ -1168,6 +1178,7 @@ Enabling this setting will disable the server config GUI. m_pCheckBoxWin32KeepForeground m_pCheckBoxIgnoreAutoConfigClient m_pCheckBoxDisableLockToScreen + m_pCheckBoxTouchActivateScreen m_pCheckBoxCornerTopLeft m_pCheckBoxCornerBottomLeft m_pCheckBoxCornerTopRight diff --git a/src/lib/base/EventTypes.cpp b/src/lib/base/EventTypes.cpp index 1c0d990b1..85896a3c7 100644 --- a/src/lib/base/EventTypes.cpp +++ b/src/lib/base/EventTypes.cpp @@ -115,6 +115,7 @@ REGISTER_EVENT(ClientListener, connected) REGISTER_EVENT(ClientProxy, ready) REGISTER_EVENT(ClientProxy, disconnected) +REGISTER_EVENT(ClientProxy, grabScreen) // // ClientProxyUnknown @@ -167,6 +168,7 @@ REGISTER_EVENT(IPrimaryScreen, hotKeyDown) REGISTER_EVENT(IPrimaryScreen, hotKeyUp) REGISTER_EVENT(IPrimaryScreen, fakeInputBegin) REGISTER_EVENT(IPrimaryScreen, fakeInputEnd) +REGISTER_EVENT(IPrimaryScreen, touchActivatedPrimary) // // IScreen @@ -176,6 +178,7 @@ REGISTER_EVENT(IScreen, error) REGISTER_EVENT(IScreen, shapeChanged) REGISTER_EVENT(IScreen, suspend) REGISTER_EVENT(IScreen, resume) +REGISTER_EVENT(IScreen, grabScreen) // // IpcServer diff --git a/src/lib/base/EventTypes.h b/src/lib/base/EventTypes.h index 6ddfe8adf..8885f1692 100644 --- a/src/lib/base/EventTypes.h +++ b/src/lib/base/EventTypes.h @@ -388,7 +388,7 @@ class ClientListenerEvents : public EventTypes class ClientProxyEvents : public EventTypes { public: - ClientProxyEvents() : m_ready(Event::kUnknown), m_disconnected(Event::kUnknown) + ClientProxyEvents() : m_ready(Event::kUnknown), m_disconnected(Event::kUnknown), m_grabScreen(Event::kUnknown) { } @@ -410,11 +410,20 @@ class ClientProxyEvents : public EventTypes */ Event::Type disconnected(); + //! Get grab screen event type + /*! + Returns the grab screen event type. This is sent when a client + requests to become the active screen (e.g., due to touch input). + Event data is MotionInfo* with the position where activation occurred. + */ + Event::Type grabScreen(); + //@} private: Event::Type m_ready; Event::Type m_disconnected; + Event::Type m_grabScreen; }; class ClientProxyUnknownEvents : public EventTypes @@ -603,7 +612,8 @@ class IPrimaryScreenEvents : public EventTypes m_hotKeyDown(Event::kUnknown), m_hotKeyUp(Event::kUnknown), m_fakeInputBegin(Event::kUnknown), - m_fakeInputEnd(Event::kUnknown) + m_fakeInputEnd(Event::kUnknown), + m_touchActivatedPrimary(Event::kUnknown) { } @@ -650,6 +660,13 @@ class IPrimaryScreenEvents : public EventTypes //! end of fake input event type Event::Type fakeInputEnd(); + //! touch activated primary screen event type + /*! + Event data is MotionInfo* with the position where touch occurred. + This is sent when touch input on the primary screen should activate it. + */ + Event::Type touchActivatedPrimary(); + //@} private: @@ -664,6 +681,7 @@ class IPrimaryScreenEvents : public EventTypes Event::Type m_hotKeyUp; Event::Type m_fakeInputBegin; Event::Type m_fakeInputEnd; + Event::Type m_touchActivatedPrimary; }; class IScreenEvents : public EventTypes @@ -673,7 +691,8 @@ class IScreenEvents : public EventTypes : m_error(Event::kUnknown), m_shapeChanged(Event::kUnknown), m_suspend(Event::kUnknown), - m_resume(Event::kUnknown) + m_resume(Event::kUnknown), + m_grabScreen(Event::kUnknown) { } @@ -708,6 +727,14 @@ class IScreenEvents : public EventTypes */ Event::Type resume(); + //! Get grab screen event type + /*! + Returns the grab screen event type. This is sent when a secondary screen + requests to become the active screen (e.g., due to touch input). + Event data is MotionInfo* with the position where activation occurred. + */ + Event::Type grabScreen(); + //@} private: @@ -715,6 +742,7 @@ class IScreenEvents : public EventTypes Event::Type m_shapeChanged; Event::Type m_suspend; Event::Type m_resume; + Event::Type m_grabScreen; }; class ClipboardEvents : public EventTypes diff --git a/src/lib/client/Client.cpp b/src/lib/client/Client.cpp index 88568ff96..552ef0e88 100644 --- a/src/lib/client/Client.cpp +++ b/src/lib/client/Client.cpp @@ -29,6 +29,7 @@ #include "deskflow/DropHelper.h" #include "deskflow/FileChunk.h" #include "deskflow/IPlatformScreen.h" +#include "deskflow/IPrimaryScreen.h" #include "deskflow/PacketStreamFilter.h" #include "deskflow/ProtocolUtil.h" #include "deskflow/Screen.h" @@ -87,6 +88,10 @@ Client::Client( m_events->adoptHandler( m_events->forIScreen().resume(), getEventTarget(), new TMethodEventJob(this, &Client::handleResume) ); + m_events->adoptHandler( + m_events->forIScreen().grabScreen(), m_screen->getEventTarget(), + new TMethodEventJob(this, &Client::handleGrabScreen) + ); if (m_args.m_enableDragDrop) { m_events->adoptHandler( @@ -107,6 +112,7 @@ Client::~Client() m_events->removeHandler(m_events->forIScreen().suspend(), getEventTarget()); m_events->removeHandler(m_events->forIScreen().resume(), getEventTarget()); + m_events->removeHandler(m_events->forIScreen().grabScreen(), m_screen->getEventTarget()); cleanupTimer(); cleanupScreen(); @@ -737,6 +743,16 @@ void Client::handleResume(const Event &, void *) } } +void Client::handleGrabScreen(const Event &event, void *) +{ + // Forward grab screen request to server via protocol + IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); + if (m_server != NULL) { + LOG((CLOG_DEBUG1 "requesting screen grab at %d,%d", info->m_x, info->m_y)); + m_server->grabScreen(info->m_x, info->m_y); + } +} + void Client::handleFileChunkSending(const Event &event, void *) { sendFileChunk(event.getDataObject()); diff --git a/src/lib/client/Client.h b/src/lib/client/Client.h index 7460b0bb1..79fa95b70 100644 --- a/src/lib/client/Client.h +++ b/src/lib/client/Client.h @@ -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 handleGrabScreen(const Event &event, void *); void handleFileChunkSending(const Event &, void *); void handleFileRecieveCompleted(const Event &, void *); void handleStopRetry(const Event &, void *); diff --git a/src/lib/client/ServerProxy.cpp b/src/lib/client/ServerProxy.cpp index e0b4cb853..e57a6e1c4 100644 --- a/src/lib/client/ServerProxy.cpp +++ b/src/lib/client/ServerProxy.cpp @@ -372,6 +372,12 @@ bool ServerProxy::onGrabClipboard(ClipboardID id) return true; } +void ServerProxy::grabScreen(SInt32 x, SInt32 y) +{ + LOG((CLOG_DEBUG1 "requesting screen grab at %d,%d", x, y)); + ProtocolUtil::writef(m_stream, kMsgCGrabScreen, x, y); +} + void ServerProxy::onClipboardChanged(ClipboardID id, const IClipboard *clipboard) { String data = IClipboard::marshall(clipboard); diff --git a/src/lib/client/ServerProxy.h b/src/lib/client/ServerProxy.h index 743c3393f..c02816e3e 100644 --- a/src/lib/client/ServerProxy.h +++ b/src/lib/client/ServerProxy.h @@ -61,6 +61,13 @@ class ServerProxy bool onGrabClipboard(ClipboardID); void onClipboardChanged(ClipboardID, const IClipboard *); + //! Request to grab screen + /*! + Sends a request to the server to make this client the active screen. + This is typically called when touch input is detected on the client. + */ + void grabScreen(SInt32 x, SInt32 y); + //@} // sending file chunk to server diff --git a/src/lib/deskflow/option_types.h b/src/lib/deskflow/option_types.h index 99db87a47..20135995c 100644 --- a/src/lib/deskflow/option_types.h +++ b/src/lib/deskflow/option_types.h @@ -69,6 +69,7 @@ static const OptionID kOptionWin32KeepForeground = OPTION_CODE("_KFW"); static const OptionID kOptionDisableLockToScreen = OPTION_CODE("DLTS"); static const OptionID kOptionClipboardSharing = OPTION_CODE("CLPS"); static const OptionID kOptionClipboardSharingSize = OPTION_CODE("CLSZ"); +static const OptionID kOptionTouchActivateScreen = OPTION_CODE("TILC"); //@} //! @name Screen switch corner enumeration diff --git a/src/lib/deskflow/protocol_types.cpp b/src/lib/deskflow/protocol_types.cpp index 72cbf0973..fbe6a4116 100644 --- a/src/lib/deskflow/protocol_types.cpp +++ b/src/lib/deskflow/protocol_types.cpp @@ -50,6 +50,7 @@ const char *const kMsgDFileTransfer = "DFTR%1i%s"; const char *const kMsgDDragInfo = "DDRG%2i%s"; const char *const kMsgDSecureInputNotification = "SECN%s"; const char *const kMsgDLanguageSynchronisation = "LSYN%s"; +const char *const kMsgCGrabScreen = "CGRB%2i%2i"; const char *const kMsgQInfo = "QINF"; const char *const kMsgEIncompatible = "EICV%2i%2i"; const char *const kMsgEBusy = "EBSY"; diff --git a/src/lib/deskflow/protocol_types.h b/src/lib/deskflow/protocol_types.h index 5c67bb161..6b8e609fd 100644 --- a/src/lib/deskflow/protocol_types.h +++ b/src/lib/deskflow/protocol_types.h @@ -294,6 +294,11 @@ extern const char *const kMsgDSecureInputNotification; // $1 = List of server languages extern const char *const kMsgDLanguageSynchronisation; +// grab screen request: secondary -> primary +// Client requests to become the active screen (e.g., due to touch input). +// $1 = x position, $2 = y position where activation occurred +extern const char *const kMsgCGrabScreen; + // // query codes // diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index c9afc4d31..85e4e35e6 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -414,20 +414,12 @@ LRESULT CALLBACK MSWindowsDesks::primaryDeskProc(HWND hwnd, UINT msg, WPARAM wPa LRESULT CALLBACK MSWindowsDesks::secondaryDeskProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { - // would like to detect any local user input and hide the hider - // window but for now we just detect mouse motion. - bool hide = false; switch (msg) { - case WM_MOUSEMOVE: - if (LOWORD(lParam) != 0 || HIWORD(lParam) != 0) { - hide = true; - } - break; - } - - if (hide && IsWindowVisible(hwnd)) { - ReleaseCapture(); - SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_HIDEWINDOW); + case WM_SETCURSOR: + // Force blank cursor. On touchscreen devices, ShowCursor(FALSE) may + // not reliably hide the cursor, so we also set a NULL cursor here. + SetCursor(NULL); + return TRUE; } return DefWindowProc(hwnd, msg, wParam, lParam); @@ -519,6 +511,10 @@ void MSWindowsDesks::deskEnter(Desk *desk) { if (!m_isPrimary) { ReleaseCapture(); + + // Restore WS_EX_TRANSPARENT that was removed in deskLeave + LONG_PTR exStyle = GetWindowLongPtr(desk->m_window, GWL_EXSTYLE); + SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle | WS_EX_TRANSPARENT); } setCursorVisibility(true); @@ -600,23 +596,21 @@ void MSWindowsDesks::deskLeave(Desk *desk, HKL keyLayout) } } } else { - // move hider window under the cursor center, raise, and show it - SetWindowPos(desk->m_window, HWND_TOP, m_xCenter, m_yCenter, 1, 1, SWP_NOACTIVATE | SWP_SHOWWINDOW); + // Remove WS_EX_TRANSPARENT so the hider window receives hit-testing + // and its blank cursor class applies. Without this, hit-testing + // passes through and the cursor of the window behind is shown. + LONG_PTR exStyle = GetWindowLongPtr(desk->m_window, GWL_EXSTYLE); + SetWindowLongPtr(desk->m_window, GWL_EXSTYLE, exStyle & ~WS_EX_TRANSPARENT); + + // Cover the entire screen with the hider window so the blank cursor + // class applies everywhere. On touchscreen devices, ShowCursor(FALSE) + // is unreliable, so the blank cursor on a full-screen window is the + // primary hiding mechanism. + SetWindowPos(desk->m_window, HWND_TOPMOST, m_x, m_y, m_w, m_h, SWP_NOACTIVATE | SWP_SHOWWINDOW); - // watch for mouse motion. if we see any then we hide the - // hider window so the user can use the physically attached - // mouse if desired. we'd rather not capture the mouse but - // we aren't notified when the mouse leaves our window. SetCapture(desk->m_window); - // windows can take a while to hide the cursor, so wait a few milliseconds to ensure the cursor - // is hidden before centering. this doesn't seem to affect the fluidity of the transition. - // without this, the cursor appears to flicker in the center of the screen which is annoying. - // a slightly more elegant but complex solution could be to use a timed event. - // 30 ms seems to work well enough without making the transition feel janky; a lower number - // would be better but 10 ms doesn't seem to be quite long enough, as we get noticeable flicker. - // this is largely a balance and out of our control, since windows can be unpredictable... - // maybe another approach would be to repeatedly check the cursor visibility until it is hidden. + // Brief delay for cursor hiding to take effect, then center the cursor. LOG_DEBUG1("centering cursor on leave: %+d,%+d", m_xCenter, m_yCenter); ARCH->sleep(0.03); deskMouseMove(m_xCenter, m_yCenter); @@ -640,6 +634,33 @@ void MSWindowsDesks::deskThread(void *vdesk) try { desk->m_window = createWindow(m_deskClass, DESKFLOW_APP_NAME "Desk"); LOG((CLOG_DEBUG "desk %s window is 0x%08x", desk->m_name.c_str(), desk->m_window)); + + // Register for raw touch input on the desk window. This MUST be on + // the desk thread (not the main thread) because the main event loop + // uses QS_ALLPOSTMESSAGE which never wakes for WM_INPUT messages. + // The desk thread's GetMessage(NULL,0,0) has no such filter. + RAWINPUTDEVICE rids[4] = {}; + rids[0].usUsagePage = 0x0D; rids[0].usUsage = 0x04; // Touch Screen + rids[0].dwFlags = RIDEV_INPUTSINK; rids[0].hwndTarget = desk->m_window; + rids[1].usUsagePage = 0x0D; rids[1].usUsage = 0x05; // Touch Pad + rids[1].dwFlags = RIDEV_INPUTSINK; rids[1].hwndTarget = desk->m_window; + rids[2].usUsagePage = 0x0D; rids[2].usUsage = 0x01; // Digitizer + rids[2].dwFlags = RIDEV_INPUTSINK; rids[2].hwndTarget = desk->m_window; + rids[3].usUsagePage = 0x0D; rids[3].usUsage = 0x02; // Pen + rids[3].dwFlags = RIDEV_INPUTSINK; rids[3].hwndTarget = desk->m_window; + if (RegisterRawInputDevices(rids, 4, sizeof(RAWINPUTDEVICE))) { + LOG((CLOG_DEBUG "desk %s: registered raw touch input on desk window", + desk->m_name.c_str())); + } else { + // fallback: try just touch screen + if (RegisterRawInputDevices(rids, 1, sizeof(RAWINPUTDEVICE))) { + LOG((CLOG_DEBUG "desk %s: registered touch screen raw input", + desk->m_name.c_str())); + } else { + LOG((CLOG_WARN "desk %s: failed to register raw touch input, error=%d", + desk->m_name.c_str(), GetLastError())); + } + } } catch (...) { // ignore LOG((CLOG_DEBUG "can't create desk window for %s", desk->m_name.c_str())); @@ -660,6 +681,32 @@ void MSWindowsDesks::deskThread(void *vdesk) DispatchMessage(&msg); continue; + case WM_INPUT: { + // Raw touch input from digitizer. Forward to main thread as + // DESKFLOW_MSG_TOUCH for the same handling as the LL mouse hook. + UINT size = 0; + GetRawInputData( + reinterpret_cast(msg.lParam), RID_INPUT, + NULL, &size, sizeof(RAWINPUTHEADER)); + if (size > 0 && size <= 1024) { + BYTE buffer[1024]; + if (GetRawInputData( + reinterpret_cast(msg.lParam), RID_INPUT, + buffer, &size, sizeof(RAWINPUTHEADER)) != static_cast(-1)) { + RAWINPUT *raw = reinterpret_cast(buffer); + if (raw->header.dwType == RIM_TYPEHID && + raw->data.hid.dwCount > 0 && raw->data.hid.dwSizeHid > 0) { + POINT pt; + GetCursorPos(&pt); + LOG((CLOG_DEBUG1 "desk raw touch at %d,%d", pt.x, pt.y)); + PostThreadMessage(m_threadID, DESKFLOW_MSG_TOUCH, + static_cast(pt.x), static_cast(pt.y)); + } + } + } + continue; + } + case DESKFLOW_MSG_SWITCH: if (!m_noHooks) { MSWindowsHook::uninstall(); diff --git a/src/lib/platform/MSWindowsHook.cpp b/src/lib/platform/MSWindowsHook.cpp index b51b1a734..e042fab41 100644 --- a/src/lib/platform/MSWindowsHook.cpp +++ b/src/lib/platform/MSWindowsHook.cpp @@ -44,6 +44,13 @@ static BYTE g_keyState[256] = {0}; static DWORD g_hookThread = 0; static bool g_fakeServerInput = false; static BOOL g_isPrimary = TRUE; +static bool g_touchActivateScreen = false; + +// Microsoft touch signature in dwExtraInfo (MI_WP_SIGNATURE). +// The upper 24 bits (masked by 0xFFFFFF00) identify touch-generated +// mouse events; the lower 8 bits contain pen/touch flags. +#define TOUCH_SIGNATURE_MASK 0xFFFFFF00 +#define TOUCH_SIGNATURE 0xFF515700 MSWindowsHook::MSWindowsHook() { @@ -148,6 +155,16 @@ void MSWindowsHook::setMode(EHookMode mode) g_mode = mode; } +void MSWindowsHook::setTouchActivateScreen(bool enabled) +{ + g_touchActivateScreen = enabled; +} + +void MSWindowsHook::setIsPrimary(bool primary) +{ + g_isPrimary = primary ? TRUE : FALSE; +} + static void keyboardGetState(BYTE keys[256], DWORD vkCode, bool kf_up) { // we have to use GetAsyncKeyState() rather than GetKeyState() because @@ -580,6 +597,25 @@ static LRESULT CALLBACK mouseLLHook(int code, WPARAM wParam, LPARAM lParam) // decode the message MSLLHOOKSTRUCT *info = reinterpret_cast(lParam); + // detect touch-originated mouse events via dwExtraInfo signature. + // this must run before the injected check, because Windows marks + // touch-synthesized mouse events as injected (LLMHF_INJECTED). + if (g_touchActivateScreen) { + bool isTouchEvent = (info->dwExtraInfo & TOUCH_SIGNATURE_MASK) == TOUCH_SIGNATURE; + if (isTouchEvent && (wParam == WM_LBUTTONDOWN || wParam == WM_MOUSEMOVE)) { + SInt32 x = static_cast(info->pt.x); + SInt32 y = static_cast(info->pt.y); + PostThreadMessage(g_threadID, DESKFLOW_MSG_TOUCH, x, y); + // On primary: eat the event to prevent edge detection and + // button-state locking (isLockedToScreen) from racing. + // On secondary (client): let it through so the click reaches + // the target window (e.g. Start menu) — no jump zones on clients. + if (g_isPrimary) { + return 1; + } + } + } + bool const injected = info->flags & LLMHF_INJECTED; if (!g_isPrimary && injected) { return CallNextHookEx(g_mouseLL, code, wParam, lParam); diff --git a/src/lib/platform/MSWindowsHook.h b/src/lib/platform/MSWindowsHook.h index 51684395f..8aac00950 100644 --- a/src/lib/platform/MSWindowsHook.h +++ b/src/lib/platform/MSWindowsHook.h @@ -49,4 +49,8 @@ class MSWindowsHook static int installScreenSaver(); static int uninstallScreenSaver(); + + void setTouchActivateScreen(bool enabled); + + void setIsPrimary(bool primary); }; diff --git a/src/lib/platform/MSWindowsScreen.cpp b/src/lib/platform/MSWindowsScreen.cpp index f585110cf..46be497bf 100644 --- a/src/lib/platform/MSWindowsScreen.cpp +++ b/src/lib/platform/MSWindowsScreen.cpp @@ -32,6 +32,7 @@ #include "deskflow/Clipboard.h" #include "deskflow/KeyMap.h" #include "deskflow/XScreen.h" +#include "deskflow/option_types.h" #include "mt/Thread.h" #include "platform/MSWindowsClipboard.h" #include "platform/MSWindowsDesks.h" @@ -83,6 +84,28 @@ #define PBT_APMRESUMEAUTOMATIC 0x0012 #endif +// WM_POINTER stuff (Windows 8+) +#if !defined(WM_POINTERDOWN) +#define WM_POINTERDOWN 0x0246 +#define WM_POINTERUP 0x0247 +#define WM_POINTERUPDATE 0x0245 +#define WM_POINTERENTER 0x0249 +#define WM_POINTERLEAVE 0x024A +#define GET_POINTERID_WPARAM(wParam) (LOWORD(wParam)) +#endif + +#if !defined(PT_POINTER) +#define PT_POINTER 1 +#define PT_TOUCH 2 +#define PT_PEN 3 +#define PT_MOUSE 4 +#endif + +// Function pointer type for GetPointerType (loaded dynamically for Win7 compat) +typedef BOOL(WINAPI *GetPointerTypeFunc)(UINT32 pointerId, DWORD *pointerType); +static GetPointerTypeFunc s_getPointerType = NULL; +static bool s_pointerApiChecked = false; + // // MSWindowsScreen // @@ -124,7 +147,9 @@ MSWindowsScreen::MSWindowsScreen( m_hasMouse(GetSystemMetrics(SM_MOUSEPRESENT) != 0), m_events(events), m_dropWindow(NULL), - m_dropWindowSize(20) + m_dropWindowSize(20), + m_touchActivateScreen(false), + m_touchDebounceTimer() { LOG_DEBUG("settting up %s screen", m_isPrimary ? "primary" : "secondary"); @@ -133,8 +158,9 @@ MSWindowsScreen::MSWindowsScreen( s_screen = this; try { - if (m_isPrimary && !m_noHooks) { + if (!m_noHooks) { m_hook.loadLibrary(); + m_hook.setIsPrimary(m_isPrimary); } m_screensaver = new MSWindowsScreenSaver(); @@ -150,6 +176,7 @@ MSWindowsScreen::MSWindowsScreen( m_class = createWindowClass(); m_window = createWindow(m_class, DESKFLOW_APP_NAME); setupMouseKeys(); + LOG((CLOG_DEBUG "screen shape: %d,%d %dx%d %s", m_x, m_y, m_w, m_h, m_multimon ? "(multi-monitor)" : "")); LOG((CLOG_DEBUG "window is 0x%08x", m_window)); @@ -476,6 +503,15 @@ void MSWindowsScreen::resetOptions() void MSWindowsScreen::setOptions(const OptionsList &options) { m_desks->setOptions(options); + + // check for touch input local option + for (UInt32 i = 0, n = (UInt32)options.size(); i < n; i += 2) { + if (options[i] == kOptionTouchActivateScreen) { + m_touchActivateScreen = (options[i + 1] != 0); + m_hook.setTouchActivateScreen(m_touchActivateScreen); + LOG((CLOG_DEBUG "touch activate screen set to %s", m_touchActivateScreen ? "true" : "false")); + } + } } void MSWindowsScreen::setSequenceNumber(UInt32 seqNum) @@ -957,6 +993,30 @@ bool MSWindowsScreen::onPreDispatch(HWND hwnd, UINT message, WPARAM wParam, LPAR case DESKFLOW_MSG_DEBUG: LOG((CLOG_DEBUG1 "hook: 0x%08x 0x%08x", wParam, lParam)); return true; + + case DESKFLOW_MSG_TOUCH: + // Thread messages (PostThreadMessage) bypass DispatchMessage, so + // this must be handled here in onPreDispatch, not in onEvent. + if (!m_touchActivateScreen || m_isOnScreen) + return true; + { + if (m_touchDebounceTimer.getTime() < kTouchDebounceTime) + return true; + m_touchDebounceTimer.reset(); + + SInt32 x = static_cast(wParam); + SInt32 y = static_cast(lParam); + if (m_isPrimary) { + LOG((CLOG_INFO "hook: touch activating primary screen at %d,%d", x, y)); + sendEvent(m_events->forIPrimaryScreen().touchActivatedPrimary(), + MotionInfo::alloc(x, y)); + } else { + LOG((CLOG_INFO "hook: touch requesting screen grab at %d,%d", x, y)); + sendEvent(m_events->forIScreen().grabScreen(), + MotionInfo::alloc(x, y)); + } + } + return true; } if (m_isPrimary) { @@ -1043,6 +1103,17 @@ bool MSWindowsScreen::onEvent(HWND, UINT msg, WPARAM wParam, LPARAM lParam, LRES case WM_DISPLAYCHANGE: return onDisplayChange(); + case WM_POINTERDOWN: + case WM_POINTERUP: + case WM_POINTERUPDATE: + if (onPointerInput(wParam, lParam)) { + // Touch input was consumed (kept local or triggered screen switch) + *result = 0; + return true; + } + // Fall through to let DefWindowProc convert to mouse messages + return false; + /* On windows 10 we don't receive WM_POWERBROADCAST after sleep. We receive only WM_TIMECHANGE hence this message is used to resume.*/ case WM_TIMECHANGE: @@ -1408,6 +1479,60 @@ bool MSWindowsScreen::onScreensaver(bool activated) return true; } +bool MSWindowsScreen::isPointerTypeTouch(UINT32 pointerId) const +{ + // Dynamically load GetPointerType for Windows 7 compatibility + if (!s_pointerApiChecked) { + s_pointerApiChecked = true; + HMODULE user32 = GetModuleHandle("user32.dll"); + if (user32 != NULL) { + s_getPointerType = (GetPointerTypeFunc)GetProcAddress(user32, "GetPointerType"); + } + } + + if (s_getPointerType == NULL) { + // API not available (Windows 7 or earlier) + return false; + } + + DWORD pointerType = PT_POINTER; + if (s_getPointerType(pointerId, &pointerType)) { + return (pointerType == PT_TOUCH || pointerType == PT_PEN); + } + return false; +} + +bool MSWindowsScreen::onPointerInput(WPARAM wParam, LPARAM lParam) +{ + UINT32 pointerId = GET_POINTERID_WPARAM(wParam); + + if (!isPointerTypeTouch(pointerId)) + return false; + + if (!m_touchActivateScreen || m_isOnScreen) + return false; + + if (m_touchDebounceTimer.getTime() < kTouchDebounceTime) + return true; + m_touchDebounceTimer.reset(); + + POINT pt; + if (!GetCursorPos(&pt)) + return false; + + if (m_isPrimary) { + LOG((CLOG_INFO "touch activating primary screen at %d,%d", pt.x, pt.y)); + sendEvent(m_events->forIPrimaryScreen().touchActivatedPrimary(), + MotionInfo::alloc(pt.x, pt.y)); + } else { + LOG((CLOG_INFO "touch requesting screen grab at %d,%d", pt.x, pt.y)); + sendEvent(m_events->forIScreen().grabScreen(), + MotionInfo::alloc(pt.x, pt.y)); + } + + return true; +} + bool MSWindowsScreen::onDisplayChange() { // screen resolution may have changed. save old shape. diff --git a/src/lib/platform/MSWindowsScreen.h b/src/lib/platform/MSWindowsScreen.h index 8688d4286..9c98089ee 100644 --- a/src/lib/platform/MSWindowsScreen.h +++ b/src/lib/platform/MSWindowsScreen.h @@ -18,6 +18,7 @@ #pragma once +#include "base/Stopwatch.h" #include "base/String.h" #include "deskflow/ClientArgs.h" #include "deskflow/DragInformation.h" @@ -190,6 +191,8 @@ class MSWindowsScreen : public PlatformScreen bool onScreensaver(bool activated); bool onDisplayChange(); bool onClipboardChange(); + bool onPointerInput(WPARAM wParam, LPARAM lParam); + bool isPointerTypeTouch(UINT32 pointerId) const; // warp cursor without discarding queued events void warpCursorNoFlush(SInt32 x, SInt32 y); @@ -357,4 +360,11 @@ class MSWindowsScreen : public PlatformScreen PrimaryKeyDownList m_primaryKeyDownList; MSWindowsPowerManager m_powerManager; + + // When true, touching this screen activates it (switches focus here) + bool m_touchActivateScreen; + + // Debounce rapid touch events to prevent multiple switch requests + Stopwatch m_touchDebounceTimer; + static constexpr double kTouchDebounceTime = 0.15; // 150ms debounce }; diff --git a/src/lib/platform/dfwhook.h b/src/lib/platform/dfwhook.h index 8822663ae..7280ac5fb 100644 --- a/src/lib/platform/dfwhook.h +++ b/src/lib/platform/dfwhook.h @@ -46,9 +46,10 @@ #define DESKFLOW_MSG_PRE_WARP WM_APP + 0x0017 // x; y #define DESKFLOW_MSG_SCREEN_SAVER WM_APP + 0x0018 // activated; #define DESKFLOW_MSG_DEBUG WM_APP + 0x0019 // data, data +#define DESKFLOW_MSG_TOUCH WM_APP + 0x001A // x; y (touch-originated mouse event) #define DESKFLOW_MSG_INPUT_FIRST DESKFLOW_MSG_KEY #define DESKFLOW_MSG_INPUT_LAST DESKFLOW_MSG_PRE_WARP -#define DESKFLOW_HOOK_LAST_MSG DESKFLOW_MSG_DEBUG +#define DESKFLOW_HOOK_LAST_MSG DESKFLOW_MSG_TOUCH #define DESKFLOW_HOOK_FAKE_INPUT_VIRTUAL_KEY VK_CANCEL #define DESKFLOW_HOOK_FAKE_INPUT_SCANCODE 0 diff --git a/src/lib/server/ClientProxy1_0.cpp b/src/lib/server/ClientProxy1_0.cpp index 31b51020f..7d1deef57 100644 --- a/src/lib/server/ClientProxy1_0.cpp +++ b/src/lib/server/ClientProxy1_0.cpp @@ -21,6 +21,7 @@ #include "base/IEventQueue.h" #include "base/Log.h" #include "base/TMethodEventJob.h" +#include "deskflow/IPrimaryScreen.h" #include "deskflow/ProtocolUtil.h" #include "deskflow/XDeskflow.h" #include "io/IStream.h" @@ -188,10 +189,31 @@ bool ClientProxy1_0::parseMessage(const UInt8 *code) return recvGrabClipboard(); } else if (memcmp(code, kMsgDClipboard, 4) == 0) { return recvClipboard(); + } else if (memcmp(code, kMsgCGrabScreen, 4) == 0) { + return recvGrabScreen(); } return false; } +bool ClientProxy1_0::recvGrabScreen() +{ + // parse message + SInt16 x, y; + if (!ProtocolUtil::readf(getStream(), kMsgCGrabScreen + 4, &x, &y)) { + return false; + } + LOG((CLOG_DEBUG "received client \"%s\" grab screen request at %d,%d", getName().c_str(), x, y)); + + // notify server to switch to this client + m_events->addEvent(Event( + m_events->forClientProxy().grabScreen(), + getEventTarget(), + IPrimaryScreen::MotionInfo::alloc(x, y) + )); + + return true; +} + void ClientProxy1_0::handleDisconnect(const Event &, void *) { LOG((CLOG_NOTE "client \"%s\" has disconnected", getName().c_str())); diff --git a/src/lib/server/ClientProxy1_0.h b/src/lib/server/ClientProxy1_0.h index 0339b44e6..bfa6e4e71 100644 --- a/src/lib/server/ClientProxy1_0.h +++ b/src/lib/server/ClientProxy1_0.h @@ -87,6 +87,7 @@ class ClientProxy1_0 : public ClientProxy bool recvInfo(); bool recvGrabClipboard(); + bool recvGrabScreen(); protected: struct ClientClipboard diff --git a/src/lib/server/Config.cpp b/src/lib/server/Config.cpp index aeeef31b4..b72eaa226 100644 --- a/src/lib/server/Config.cpp +++ b/src/lib/server/Config.cpp @@ -699,6 +699,8 @@ void Config::readSectionOptions(ConfigReadContext &s) addOption("", kOptionClipboardSharing, s.parseBoolean(value)); } else if (name == "clipboardSharingSize") { addOption("", kOptionClipboardSharingSize, s.parseInt(value)); + } else if (name == "touchActivateScreen" || name == "touchInputLocal") { + addOption("", kOptionTouchActivateScreen, s.parseBoolean(value)); } else if (name == "clientAddress") { m_ClientAddress = value; } else { diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index 43a5fbae1..a8651360a 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -40,12 +40,18 @@ #include "server/ClientProxyUnknown.h" #include "server/PrimaryClient.h" +#include #include #include #include #include #include +#if WINAPI_MSWINDOWS +#define WIN32_LEAN_AND_MEAN +#include +#endif + using namespace deskflow::server; // @@ -176,6 +182,10 @@ Server::Server( m_events->forIPrimaryScreen().fakeInputEnd(), m_inputFilter, new TMethodEventJob(this, &Server::handleFakeInputEndEvent) ); + m_events->adoptHandler( + m_events->forIPrimaryScreen().touchActivatedPrimary(), m_primaryClient->getEventTarget(), + new TMethodEventJob(this, &Server::handleTouchActivatedPrimaryEvent) + ); if (m_args.m_enableDragDrop) { m_events->adoptHandler( @@ -225,6 +235,7 @@ Server::~Server() m_events->removeHandler(m_events->forIPrimaryScreen().screensaverDeactivated(), m_primaryClient->getEventTarget()); m_events->removeHandler(m_events->forIPrimaryScreen().fakeInputBegin(), m_inputFilter); m_events->removeHandler(m_events->forIPrimaryScreen().fakeInputEnd(), m_inputFilter); + m_events->removeHandler(m_events->forIPrimaryScreen().touchActivatedPrimary(), m_primaryClient->getEventTarget()); m_events->removeHandler(Event::kTimer, this); stopSwitch(); @@ -776,6 +787,15 @@ bool Server::isSwitchOkay( return false; } + // check if we're in touch switch cooldown period + // this prevents edge-triggered switches from immediately undoing + // a touch-triggered switch (which causes rapid bounce switching) + if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { + LOG((CLOG_DEBUG1 "edge switch blocked by touch cooldown (%.2fs remaining)", + kTouchSwitchCooldownTime - m_touchSwitchCooldown.getTime())); + return false; + } + // should we switch or not? bool preventSwitch = false; bool allowSwitch = false; @@ -1337,6 +1357,120 @@ void Server::handleSwitchInDirectionEvent(const Event &event, void *) } } +void Server::handleTouchActivatedPrimaryEvent(const Event &event, void *) +{ + IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); + LOG((CLOG_DEBUG1 "touch activated primary at %d,%d", info->m_x, info->m_y)); + + // reject if still in cooldown from a recent touch switch + if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { + LOG((CLOG_DEBUG1 "touch switch rejected (cooldown active)")); + return; + } + + if (m_active != m_primaryClient) { + // Save current cursor position on the screen we're leaving + // (same as jumpToScreen does for edge-triggered switches) + m_active->setJumpCursorPos(m_x, m_y); + + // Clamp touch coordinates away from screen edges to avoid landing + // in the jump zone, which would trigger an immediate edge switch + SInt32 x = info->m_x; + SInt32 y = info->m_y; + SInt32 dx, dy, dw, dh; + m_primaryClient->getShape(dx, dy, dw, dh); + SInt32 z = getJumpZoneSize(m_primaryClient) + 1; + x = (std::max)(x, dx + z); + x = (std::min)(x, dx + dw - 1 - z); + y = (std::max)(y, dy + z); + y = (std::min)(y, dy + dh - 1 - z); + + // Switch back to primary screen at clamped touch position + switchScreen(m_primaryClient, x, y, false); + + // Synthesize a click at the touch position to focus the target window. + // The hook eats the original touch event to prevent edge detection race, + // so without this the window under the touch point never receives focus. + m_primaryClient->mouseDown(kButtonLeft); + m_primaryClient->mouseUp(kButtonLeft); + + // Force the window under the touch point to the foreground. + // The hook eats the original touch event, so the target window + // never receives the click. We must explicitly activate it. + // Windows restricts SetForegroundWindow to prevent focus stealing, + // so we use AttachThreadInput to bypass the restriction. +#if WINAPI_MSWINDOWS + { + POINT pt = { x, y }; + HWND hwnd = WindowFromPoint(pt); + if (hwnd != NULL) { + HWND root = GetAncestor(hwnd, GA_ROOT); + if (root != NULL) { + DWORD foreThread = GetWindowThreadProcessId( + GetForegroundWindow(), NULL); + DWORD curThread = GetCurrentThreadId(); + if (foreThread != curThread) { + AttachThreadInput(foreThread, curThread, TRUE); + } + SetForegroundWindow(root); + if (foreThread != curThread) { + AttachThreadInput(foreThread, curThread, FALSE); + } + LOG((CLOG_DEBUG1 "touch: forced foreground window 0x%08x", root)); + } + } + } +#endif + + // Start cooldown to prevent edge-triggered switches from immediately + // undoing this touch-triggered switch + m_touchSwitchCooldown.reset(); + LOG((CLOG_DEBUG1 "touch switch cooldown started")); + } +} + +void Server::handleGrabScreenEvent(const Event &event, void *vclient) +{ + IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); + BaseClientProxy *client = static_cast(vclient); + + LOG((CLOG_DEBUG1 "client \"%s\" requests grab at %d,%d", getName(client).c_str(), info->m_x, info->m_y)); + + // reject if still in cooldown from a recent touch switch + if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { + LOG((CLOG_DEBUG1 "grab rejected (cooldown active)")); + return; + } + + if (client != m_active) { + // Save current cursor position on the screen we're leaving + // (same as jumpToScreen does for edge-triggered switches) + m_active->setJumpCursorPos(m_x, m_y); + + // Clamp touch coordinates away from screen edges to avoid landing + // in the jump zone on the primary screen (which would trigger an + // immediate edge switch back). Only matters when switching away + // from primary, since jump zones only exist on primary. + SInt32 x = info->m_x; + SInt32 y = info->m_y; + SInt32 dx, dy, dw, dh; + client->getShape(dx, dy, dw, dh); + SInt32 z = getJumpZoneSize(client) + 1; + x = (std::max)(x, dx + z); + x = (std::min)(x, dx + dw - 1 - z); + y = (std::max)(y, dy + z); + y = (std::min)(y, dy + dh - 1 - z); + + // Switch to the requesting client at clamped touch position + switchScreen(client, x, y, false); + + // Start cooldown to prevent edge-triggered switches from immediately + // undoing this touch-triggered switch + m_touchSwitchCooldown.reset(); + LOG((CLOG_DEBUG1 "touch switch cooldown started")); + } +} + void Server::handleKeyboardBroadcastEvent(const Event &event, void *) { KeyboardBroadcastInfo *info = (KeyboardBroadcastInfo *)event.getData(); @@ -1989,6 +2123,10 @@ bool Server::addClient(BaseClientProxy *client) m_events->forClipboard().clipboardChanged(), client->getEventTarget(), new TMethodEventJob(this, &Server::handleClipboardChanged, client) ); + m_events->adoptHandler( + m_events->forClientProxy().grabScreen(), client->getEventTarget(), + new TMethodEventJob(this, &Server::handleGrabScreenEvent, client) + ); // add to list m_clientSet.insert(client); @@ -2017,6 +2155,7 @@ bool Server::removeClient(BaseClientProxy *client) m_events->removeHandler(m_events->forIScreen().shapeChanged(), client->getEventTarget()); m_events->removeHandler(m_events->forClipboard().clipboardGrabbed(), client->getEventTarget()); m_events->removeHandler(m_events->forClipboard().clipboardChanged(), client->getEventTarget()); + m_events->removeHandler(m_events->forClientProxy().grabScreen(), client->getEventTarget()); // remove from list m_clients.erase(getName(client)); diff --git a/src/lib/server/Server.h b/src/lib/server/Server.h index 352539cbc..58b014a02 100644 --- a/src/lib/server/Server.h +++ b/src/lib/server/Server.h @@ -350,6 +350,8 @@ class Server : public INode void handleClientCloseTimeout(const Event &, void *); void handleSwitchToScreenEvent(const Event &, void *); void handleSwitchInDirectionEvent(const Event &, void *); + void handleTouchActivatedPrimaryEvent(const Event &, void *); + void handleGrabScreenEvent(const Event &, void *); void handleKeyboardBroadcastEvent(const Event &, void *); void handleLockCursorToScreenEvent(const Event &, void *); void handleFakeInputBeginEvent(const Event &, void *); @@ -482,6 +484,11 @@ class Server : public INode bool m_switchTwoTapArmed; SInt32 m_switchTwoTapZone; + // state for touch-triggered screen switching cooldown + // prevents edge-triggered switches from immediately undoing touch switches + Stopwatch m_touchSwitchCooldown; + static constexpr double kTouchSwitchCooldownTime = 0.5; // 500ms cooldown + // modifiers needed before switching bool m_switchNeedsShift; bool m_switchNeedsControl; From 900171f84a98adc01f560d1a7b307899fc477127 Mon Sep 17 00:00:00 2001 From: Stefan Verleysen Date: Wed, 11 Feb 2026 14:02:11 -0500 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20protocol=20versioning,=20platform=20abstraction,=20?= =?UTF-8?q?comment=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create ClientProxy1_9 for kMsgCGrabScreen (was incorrectly in ClientProxy1_0, breaking backward compatibility with older clients) - Bump protocol version 1.8 → 1.9 for touch-activated screen switching - Move Windows-specific SetForegroundWindow logic from Server.cpp to MSWindowsScreen::activateWindowAt() via platform abstraction chain - Remove dead mouseDown/mouseUp calls (PrimaryClient ignores them) - Remove #if WINAPI_MSWINDOWS and Windows.h include from Server.cpp - Clean up comments: keep only "why" comments, remove "what" comments Co-Authored-By: Claude Opus 4.6 --- src/lib/deskflow/IPlatformScreen.h | 9 ++++ src/lib/deskflow/Screen.cpp | 5 ++ src/lib/deskflow/Screen.h | 3 ++ src/lib/deskflow/protocol_types.h | 3 +- src/lib/platform/MSWindowsScreen.cpp | 27 +++++++++++ src/lib/platform/MSWindowsScreen.h | 1 + src/lib/server/ClientProxy1_0.cpp | 22 --------- src/lib/server/ClientProxy1_0.h | 1 - src/lib/server/ClientProxy1_9.cpp | 59 ++++++++++++++++++++++++ src/lib/server/ClientProxy1_9.h | 36 +++++++++++++++ src/lib/server/ClientProxyUnknown.cpp | 5 ++ src/lib/server/PrimaryClient.cpp | 5 ++ src/lib/server/PrimaryClient.h | 3 ++ src/lib/server/Server.cpp | 66 ++++----------------------- 14 files changed, 163 insertions(+), 82 deletions(-) create mode 100644 src/lib/server/ClientProxy1_9.cpp create mode 100644 src/lib/server/ClientProxy1_9.h diff --git a/src/lib/deskflow/IPlatformScreen.h b/src/lib/deskflow/IPlatformScreen.h index 1ca2ceea2..941293523 100644 --- a/src/lib/deskflow/IPlatformScreen.h +++ b/src/lib/deskflow/IPlatformScreen.h @@ -200,6 +200,15 @@ class IPlatformScreen : public IScreen, public IPrimaryScreen, public ISecondary virtual void pollPressedKeys(KeyButtonSet &pressedKeys) const = 0; virtual void clearStaleModifiers() = 0; + //! Activate the window at the given screen coordinates + /*! + Brings the window at position \c x, \c y to the foreground. + Default implementation does nothing; platforms override as needed. + */ + virtual void activateWindowAt(SInt32 x, SInt32 y) + { + } + // Drag-and-drop overrides virtual String &getDraggingFilename() = 0; virtual void clearDraggingFilename() = 0; diff --git a/src/lib/deskflow/Screen.cpp b/src/lib/deskflow/Screen.cpp index f3581201c..8af861e84 100644 --- a/src/lib/deskflow/Screen.cpp +++ b/src/lib/deskflow/Screen.cpp @@ -488,6 +488,11 @@ void Screen::leaveSecondary() m_screen->fakeAllKeysUp(); } +void Screen::activateWindowAt(SInt32 x, SInt32 y) +{ + m_screen->activateWindowAt(x, y); +} + String Screen::getSecureInputApp() const { return m_screen->getSecureInputApp(); diff --git a/src/lib/deskflow/Screen.h b/src/lib/deskflow/Screen.h index 9e4c3ef63..00ea491d8 100644 --- a/src/lib/deskflow/Screen.h +++ b/src/lib/deskflow/Screen.h @@ -236,6 +236,9 @@ class Screen : public IScreen void setEnableDragDrop(bool enabled); + //! Activate the window at the given screen coordinates + void activateWindowAt(SInt32 x, SInt32 y); + //! Determine the name of the app causing a secure input state /*! On MacOS check which app causes a secure input state to be enabled. No diff --git a/src/lib/deskflow/protocol_types.h b/src/lib/deskflow/protocol_types.h index 6b8e609fd..d158a0f89 100644 --- a/src/lib/deskflow/protocol_types.h +++ b/src/lib/deskflow/protocol_types.h @@ -31,9 +31,10 @@ // 1.6: adds clipboard streaming // 1.7 adds security input notifications // 1.8 adds language synchronization functionality +// 1.9 adds touch-activated screen switching // NOTE: with new version, deskflow minor version should increment static const SInt16 kProtocolMajorVersion = 1; -static const SInt16 kProtocolMinorVersion = 8; +static const SInt16 kProtocolMinorVersion = 9; // default contact port number static const UInt16 kDefaultPort = 24800; diff --git a/src/lib/platform/MSWindowsScreen.cpp b/src/lib/platform/MSWindowsScreen.cpp index 46be497bf..6211e5296 100644 --- a/src/lib/platform/MSWindowsScreen.cpp +++ b/src/lib/platform/MSWindowsScreen.cpp @@ -2002,6 +2002,33 @@ String MSWindowsScreen::getSecureInputApp() const return ""; } +void MSWindowsScreen::activateWindowAt(SInt32 x, SInt32 y) +{ + POINT pt = {x, y}; + HWND hwnd = WindowFromPoint(pt); + if (hwnd == NULL) { + return; + } + + HWND root = GetAncestor(hwnd, GA_ROOT); + if (root == NULL) { + return; + } + + // Windows restricts SetForegroundWindow to prevent focus stealing, + // so we use AttachThreadInput to bypass the restriction. + DWORD foreThread = GetWindowThreadProcessId(GetForegroundWindow(), NULL); + DWORD curThread = GetCurrentThreadId(); + if (foreThread != curThread) { + AttachThreadInput(foreThread, curThread, TRUE); + } + SetForegroundWindow(root); + if (foreThread != curThread) { + AttachThreadInput(foreThread, curThread, FALSE); + } + LOG((CLOG_DEBUG1 "touch: forced foreground window 0x%08x", root)); +} + bool MSWindowsScreen::isModifierRepeat(KeyModifierMask oldState, KeyModifierMask state, WPARAM wParam) const { bool result = false; diff --git a/src/lib/platform/MSWindowsScreen.h b/src/lib/platform/MSWindowsScreen.h index 9c98089ee..7bb9889a4 100644 --- a/src/lib/platform/MSWindowsScreen.h +++ b/src/lib/platform/MSWindowsScreen.h @@ -137,6 +137,7 @@ class MSWindowsScreen : public PlatformScreen virtual String &getDraggingFilename(); virtual const String &getDropTarget() const; String getSecureInputApp() const override; + void activateWindowAt(SInt32 x, SInt32 y) override; protected: // IPlatformScreen overrides diff --git a/src/lib/server/ClientProxy1_0.cpp b/src/lib/server/ClientProxy1_0.cpp index 7d1deef57..31b51020f 100644 --- a/src/lib/server/ClientProxy1_0.cpp +++ b/src/lib/server/ClientProxy1_0.cpp @@ -21,7 +21,6 @@ #include "base/IEventQueue.h" #include "base/Log.h" #include "base/TMethodEventJob.h" -#include "deskflow/IPrimaryScreen.h" #include "deskflow/ProtocolUtil.h" #include "deskflow/XDeskflow.h" #include "io/IStream.h" @@ -189,31 +188,10 @@ bool ClientProxy1_0::parseMessage(const UInt8 *code) return recvGrabClipboard(); } else if (memcmp(code, kMsgDClipboard, 4) == 0) { return recvClipboard(); - } else if (memcmp(code, kMsgCGrabScreen, 4) == 0) { - return recvGrabScreen(); } return false; } -bool ClientProxy1_0::recvGrabScreen() -{ - // parse message - SInt16 x, y; - if (!ProtocolUtil::readf(getStream(), kMsgCGrabScreen + 4, &x, &y)) { - return false; - } - LOG((CLOG_DEBUG "received client \"%s\" grab screen request at %d,%d", getName().c_str(), x, y)); - - // notify server to switch to this client - m_events->addEvent(Event( - m_events->forClientProxy().grabScreen(), - getEventTarget(), - IPrimaryScreen::MotionInfo::alloc(x, y) - )); - - return true; -} - void ClientProxy1_0::handleDisconnect(const Event &, void *) { LOG((CLOG_NOTE "client \"%s\" has disconnected", getName().c_str())); diff --git a/src/lib/server/ClientProxy1_0.h b/src/lib/server/ClientProxy1_0.h index bfa6e4e71..0339b44e6 100644 --- a/src/lib/server/ClientProxy1_0.h +++ b/src/lib/server/ClientProxy1_0.h @@ -87,7 +87,6 @@ class ClientProxy1_0 : public ClientProxy bool recvInfo(); bool recvGrabClipboard(); - bool recvGrabScreen(); protected: struct ClientClipboard diff --git a/src/lib/server/ClientProxy1_9.cpp b/src/lib/server/ClientProxy1_9.cpp new file mode 100644 index 000000000..8b0171f79 --- /dev/null +++ b/src/lib/server/ClientProxy1_9.cpp @@ -0,0 +1,59 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "server/ClientProxy1_9.h" + +#include "base/IEventQueue.h" +#include "base/Log.h" +#include "deskflow/IPrimaryScreen.h" +#include "deskflow/ProtocolUtil.h" +#include "deskflow/protocol_types.h" + +#include + +ClientProxy1_9::ClientProxy1_9( + const String &name, deskflow::IStream *adoptedStream, Server *server, IEventQueue *events +) + : ClientProxy1_8(name, adoptedStream, server, events), + m_events(events) +{ +} + +bool ClientProxy1_9::parseMessage(const UInt8 *code) +{ + if (memcmp(code, kMsgCGrabScreen, 4) == 0) { + return recvGrabScreen(); + } + return ClientProxy1_8::parseMessage(code); +} + +bool ClientProxy1_9::recvGrabScreen() +{ + SInt16 x, y; + if (!ProtocolUtil::readf(getStream(), kMsgCGrabScreen + 4, &x, &y)) { + return false; + } + LOG((CLOG_DEBUG "received client \"%s\" grab screen request at %d,%d", getName().c_str(), x, y)); + + m_events->addEvent(Event( + m_events->forClientProxy().grabScreen(), + getEventTarget(), + IPrimaryScreen::MotionInfo::alloc(x, y) + )); + + return true; +} diff --git a/src/lib/server/ClientProxy1_9.h b/src/lib/server/ClientProxy1_9.h new file mode 100644 index 000000000..8f2ed6c21 --- /dev/null +++ b/src/lib/server/ClientProxy1_9.h @@ -0,0 +1,36 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "server/ClientProxy1_8.h" + +//! Proxy for client implementing protocol version 1.9 +class ClientProxy1_9 : public ClientProxy1_8 +{ +public: + ClientProxy1_9(const String &name, deskflow::IStream *adoptedStream, Server *server, IEventQueue *events); + ~ClientProxy1_9() override = default; + +protected: + bool parseMessage(const UInt8 *code) override; + +private: + bool recvGrabScreen(); + + IEventQueue *m_events; +}; diff --git a/src/lib/server/ClientProxyUnknown.cpp b/src/lib/server/ClientProxyUnknown.cpp index 36b1bf586..14c923c95 100644 --- a/src/lib/server/ClientProxyUnknown.cpp +++ b/src/lib/server/ClientProxyUnknown.cpp @@ -36,6 +36,7 @@ #include "server/ClientProxy1_6.h" #include "server/ClientProxy1_7.h" #include "server/ClientProxy1_8.h" +#include "server/ClientProxy1_9.h" #include "server/Server.h" #include @@ -199,6 +200,10 @@ void ClientProxyUnknown::initProxy(const String &name, int major, int minor) case 8: m_proxy = new ClientProxy1_8(name, m_stream, m_server, m_events); break; + + case 9: + m_proxy = new ClientProxy1_9(name, m_stream, m_server, m_events); + break; } } diff --git a/src/lib/server/PrimaryClient.cpp b/src/lib/server/PrimaryClient.cpp index a7024daec..070d4cbbf 100644 --- a/src/lib/server/PrimaryClient.cpp +++ b/src/lib/server/PrimaryClient.cpp @@ -71,6 +71,11 @@ void PrimaryClient::fakeInputEnd() } } +void PrimaryClient::activateWindowAt(SInt32 x, SInt32 y) +{ + m_screen->activateWindowAt(x, y); +} + SInt32 PrimaryClient::getJumpZoneSize() const { return m_screen->getJumpZoneSize(); diff --git a/src/lib/server/PrimaryClient.h b/src/lib/server/PrimaryClient.h index dfab74c7f..965a0c6e4 100644 --- a/src/lib/server/PrimaryClient.h +++ b/src/lib/server/PrimaryClient.h @@ -83,6 +83,9 @@ class PrimaryClient : public BaseClientProxy */ void fakeInputEnd(); + //! Activate the window at the given screen coordinates + void activateWindowAt(SInt32 x, SInt32 y); + //@} //! @name accessors //@{ diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index a8651360a..710db4038 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -47,11 +47,6 @@ #include #include -#if WINAPI_MSWINDOWS -#define WIN32_LEAN_AND_MEAN -#include -#endif - using namespace deskflow::server; // @@ -787,8 +782,7 @@ bool Server::isSwitchOkay( return false; } - // check if we're in touch switch cooldown period - // this prevents edge-triggered switches from immediately undoing + // Cooldown prevents edge-triggered switches from immediately undoing // a touch-triggered switch (which causes rapid bounce switching) if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { LOG((CLOG_DEBUG1 "edge switch blocked by touch cooldown (%.2fs remaining)", @@ -1362,19 +1356,15 @@ void Server::handleTouchActivatedPrimaryEvent(const Event &event, void *) IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); LOG((CLOG_DEBUG1 "touch activated primary at %d,%d", info->m_x, info->m_y)); - // reject if still in cooldown from a recent touch switch if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { LOG((CLOG_DEBUG1 "touch switch rejected (cooldown active)")); return; } if (m_active != m_primaryClient) { - // Save current cursor position on the screen we're leaving - // (same as jumpToScreen does for edge-triggered switches) m_active->setJumpCursorPos(m_x, m_y); - // Clamp touch coordinates away from screen edges to avoid landing - // in the jump zone, which would trigger an immediate edge switch + // Clamp away from jump zones to avoid triggering an immediate edge switch SInt32 x = info->m_x; SInt32 y = info->m_y; SInt32 dx, dy, dw, dh; @@ -1385,45 +1375,13 @@ void Server::handleTouchActivatedPrimaryEvent(const Event &event, void *) y = (std::max)(y, dy + z); y = (std::min)(y, dy + dh - 1 - z); - // Switch back to primary screen at clamped touch position switchScreen(m_primaryClient, x, y, false); - // Synthesize a click at the touch position to focus the target window. - // The hook eats the original touch event to prevent edge detection race, - // so without this the window under the touch point never receives focus. - m_primaryClient->mouseDown(kButtonLeft); - m_primaryClient->mouseUp(kButtonLeft); - - // Force the window under the touch point to the foreground. - // The hook eats the original touch event, so the target window - // never receives the click. We must explicitly activate it. - // Windows restricts SetForegroundWindow to prevent focus stealing, - // so we use AttachThreadInput to bypass the restriction. -#if WINAPI_MSWINDOWS - { - POINT pt = { x, y }; - HWND hwnd = WindowFromPoint(pt); - if (hwnd != NULL) { - HWND root = GetAncestor(hwnd, GA_ROOT); - if (root != NULL) { - DWORD foreThread = GetWindowThreadProcessId( - GetForegroundWindow(), NULL); - DWORD curThread = GetCurrentThreadId(); - if (foreThread != curThread) { - AttachThreadInput(foreThread, curThread, TRUE); - } - SetForegroundWindow(root); - if (foreThread != curThread) { - AttachThreadInput(foreThread, curThread, FALSE); - } - LOG((CLOG_DEBUG1 "touch: forced foreground window 0x%08x", root)); - } - } - } -#endif + // The hook eats the original touch event, so the window under the + // touch point never receives it. Explicitly activate that window. + m_primaryClient->activateWindowAt(x, y); - // Start cooldown to prevent edge-triggered switches from immediately - // undoing this touch-triggered switch + // Cooldown prevents edge-triggered switches from undoing this touch switch m_touchSwitchCooldown.reset(); LOG((CLOG_DEBUG1 "touch switch cooldown started")); } @@ -1436,21 +1394,15 @@ void Server::handleGrabScreenEvent(const Event &event, void *vclient) LOG((CLOG_DEBUG1 "client \"%s\" requests grab at %d,%d", getName(client).c_str(), info->m_x, info->m_y)); - // reject if still in cooldown from a recent touch switch if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { LOG((CLOG_DEBUG1 "grab rejected (cooldown active)")); return; } if (client != m_active) { - // Save current cursor position on the screen we're leaving - // (same as jumpToScreen does for edge-triggered switches) m_active->setJumpCursorPos(m_x, m_y); - // Clamp touch coordinates away from screen edges to avoid landing - // in the jump zone on the primary screen (which would trigger an - // immediate edge switch back). Only matters when switching away - // from primary, since jump zones only exist on primary. + // Clamp away from jump zones to avoid triggering an immediate edge switch SInt32 x = info->m_x; SInt32 y = info->m_y; SInt32 dx, dy, dw, dh; @@ -1461,11 +1413,9 @@ void Server::handleGrabScreenEvent(const Event &event, void *vclient) y = (std::max)(y, dy + z); y = (std::min)(y, dy + dh - 1 - z); - // Switch to the requesting client at clamped touch position switchScreen(client, x, y, false); - // Start cooldown to prevent edge-triggered switches from immediately - // undoing this touch-triggered switch + // Cooldown prevents edge-triggered switches from undoing this touch switch m_touchSwitchCooldown.reset(); LOG((CLOG_DEBUG1 "touch switch cooldown started")); } From fa1477963bd7a3f3d32badd8583d38f670c36086 Mon Sep 17 00:00:00 2001 From: Stefan Verleysen Date: Wed, 11 Feb 2026 16:20:30 -0500 Subject: [PATCH 3/3] fix: add backward compatibility for 1.9 clients connecting to 1.8 servers A 1.9 client connecting to a 1.8 server would be rejected as incompatible. Add compat table entry so the client downgrades to 1.8 (touch-to-switch unavailable, but connection works). Co-Authored-By: Claude Opus 4.6 --- src/lib/client/Client.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/client/Client.cpp b/src/lib/client/Client.cpp index 552ef0e88..cd9760f33 100644 --- a/src/lib/client/Client.cpp +++ b/src/lib/client/Client.cpp @@ -661,7 +661,8 @@ bool Client::isCompatible(int major, int minor) const { const std::map> compatibleTable{ {6, {7, 8}}, // 1.6 is compatible with 1.7 and 1.8 - {7, {8}} // 1.7 is compatible with 1.8 + {7, {8}}, // 1.7 is compatible with 1.8 + {8, {9}} // 1.8 is compatible with 1.9 (touch-to-switch unavailable) }; bool isCompatible = false;