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..cd9760f33 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(); @@ -655,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; @@ -737,6 +744,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/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/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..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; @@ -294,6 +295,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..6211e5296 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. @@ -1877,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 8688d4286..7bb9889a4 100644 --- a/src/lib/platform/MSWindowsScreen.h +++ b/src/lib/platform/MSWindowsScreen.h @@ -18,6 +18,7 @@ #pragma once +#include "base/Stopwatch.h" #include "base/String.h" #include "deskflow/ClientArgs.h" #include "deskflow/DragInformation.h" @@ -136,6 +137,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,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_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/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..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 43a5fbae1..710db4038 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -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,14 @@ bool Server::isSwitchOkay( return false; } + // Cooldown prevents edge-triggered switches from immediately undoing + // a touch-triggered switch (which causes rapid bounce switching) + if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { + 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 +1351,76 @@ 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_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { + LOG((CLOG_DEBUG1 "touch switch rejected (cooldown active)")); + return; + } + + if (m_active != m_primaryClient) { + m_active->setJumpCursorPos(m_x, m_y); + + // Clamp away from jump zones to avoid triggering an immediate edge switch + SInt32 x = info->m_x; + SInt32 y = info->m_y; + SInt32 dx, dy, dw, dh; + m_primaryClient->getShape(dx, dy, dw, dh); + SInt32 z = getJumpZoneSize(m_primaryClient) + 1; + x = (std::max)(x, dx + z); + x = (std::min)(x, dx + dw - 1 - z); + y = (std::max)(y, dy + z); + y = (std::min)(y, dy + dh - 1 - z); + + switchScreen(m_primaryClient, x, y, false); + + // 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); + + // Cooldown prevents edge-triggered switches from undoing this touch switch + m_touchSwitchCooldown.reset(); + LOG((CLOG_DEBUG1 "touch switch cooldown started")); + } +} + +void Server::handleGrabScreenEvent(const Event &event, void *vclient) +{ + IPrimaryScreen::MotionInfo *info = static_cast(event.getData()); + BaseClientProxy *client = static_cast(vclient); + + LOG((CLOG_DEBUG1 "client \"%s\" requests grab at %d,%d", getName(client).c_str(), info->m_x, info->m_y)); + + if (m_touchSwitchCooldown.getTime() < kTouchSwitchCooldownTime) { + LOG((CLOG_DEBUG1 "grab rejected (cooldown active)")); + return; + } + + if (client != m_active) { + m_active->setJumpCursorPos(m_x, m_y); + + // Clamp away from jump zones to avoid triggering an immediate edge switch + SInt32 x = info->m_x; + SInt32 y = info->m_y; + SInt32 dx, dy, dw, dh; + 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); + + // Cooldown prevents edge-triggered switches from undoing this touch 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 +2073,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 +2105,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;